Skip to content

Latest commit

ย 

History

History
356 lines (259 loc) ยท 10.9 KB

File metadata and controls

356 lines (259 loc) ยท 10.9 KB

AI API

Alter(์ „์—ญ AI ์–ด์‹œ์Šคํ„ดํŠธ), Skill ์นดํƒˆ๋กœ๊ทธ, ๊ฐ•์˜๊ณ„ํš์„œ ๊ฒ€ํ† (ํ•˜์œ„ ํ˜ธํ™˜), ํ•™๊ธฐ ์ง€์นจ ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ, Owner์šฉ API ํ‚ค/๋ชจ๋ธ ๊ด€๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๋ผ์šฐํŠธ ํŒŒ์ผ: backend/src/routes/ai.js
์ปจํŠธ๋กค๋Ÿฌ ํŒŒ์ผ: backend/src/controllers/ai.js
๊ด€๋ จ ์„œ๋น„์Šค: backend/src/services/aiSkills.js, aiPromptPolicy.js, aiProvider.js ๋“ฑ

๊ธฐ๋ณธ ๊ฒฝ๋กœ: /api/ai

UI์—์„œ์˜ Alter ์‚ฌ์šฉ๋ฒ•์€ ์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ โ€” Alter๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.


์—”๋“œํฌ์ธํŠธ ์š”์•ฝ

๋ฉ”์„œ๋“œ ๊ฒฝ๋กœ ์„ค๋ช… ๊ถŒํ•œ
GET /api/ai/skills Skill ์นดํƒˆ๋กœ๊ทธ ์กฐํšŒ isLoggedIn
GET /api/ai/alter/skill-settings Skill๋ณ„ ์ค€๋น„ ์„ค์ •(์ง€์นจยท์ฐธ๊ณ ์ž๋ฃŒ) isLoggedIn (+ ํ•™๊ธฐ AI ์ ‘๊ทผ)
GET /api/ai/alter/conversations Alter ๋Œ€ํ™” ๋ชฉ๋ก isLoggedIn
POST /api/ai/alter/conversations Alter ๋Œ€ํ™” ์ƒ์„ฑ isLoggedIn
POST /api/ai/alter/conversations/bulk-delete ๋Œ€ํ™” ์ผ๊ด„ ์‚ญ์ œ(์†Œํ”„ํŠธ) isLoggedIn (๋ณธ์ธ)
GET /api/ai/alter/conversations/:id/messages ๋Œ€ํ™” ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก isLoggedIn (๋ณธ์ธ)
PATCH /api/ai/alter/conversations/:id ๋Œ€ํ™” ์ œ๋ชฉ ๋ณ€๊ฒฝ isLoggedIn (๋ณธ์ธ)
DELETE /api/ai/alter/conversations/:id ๋Œ€ํ™” ์‚ญ์ œ(์†Œํ”„ํŠธ) isLoggedIn (๋ณธ์ธ)
POST /api/ai/alter/attachment ์ฒจ๋ถ€ ์—…๋กœ๋“œ(ํ…์ŠคํŠธ ์ถ”์ถœ/์ด๋ฏธ์ง€ ํ‚ค) isLoggedIn (+ ํ•™๊ธฐ AI ์ ‘๊ทผ)
POST /api/ai/alter Alter ํ†ตํ•ฉ ํ„ด(Skill ๋ผ์šฐํŒ…) isLoggedIn (+ ํ•™๊ธฐ AI ์ ‘๊ทผ)
POST /api/ai/syllabus/review ๊ฐ•์˜๊ณ„ํš์„œ ์ดˆ์•ˆ(SSE, ํ•˜์œ„ ํ˜ธํ™˜) isLoggedIn (+ ํ•™๊ธฐ AI ์ ‘๊ทผ)
POST /api/ai/syllabus/guidelines-template ํ•™๊ธฐ AI ์ง€์นจ ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ isAdManager
POST /api/ai/test API ํ‚ค ์œ ํšจ์„ฑ ํ…Œ์ŠคํŠธ isOwner
POST /api/ai/models ์ œ๊ณต์ž ๋ชจ๋ธ ๋ชฉ๋ก isOwner

๋Œ€๋ถ€๋ถ„ ์—”๋“œํฌ์ธํŠธ๋Š” ์•„์นด๋ฐ๋ฏธ aiEnabled, API ํ‚ค, ํ•™๊ธฐ/ํ•™๊ต AI ์ ‘๊ทผ ๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


Skill ์นดํƒˆ๋กœ๊ทธ

GET /api/ai/skills

๊ถŒํ•œ: isLoggedIn

์‘๋‹ต (200)

{
  "skills": [
    {
      "id": "chat",
      "name": "์ฑ—๋ด‡",
      "description": "ํ•™์Šตยท์ž‘์„ฑ์— ๋Œ€ํ•œ ๋ฒ”์šฉ ๋„์šฐ๋ฏธ ๋Œ€ํ™”",
      "profile": "chat"
    },
    {
      "id": "syllabus-draft",
      "name": "์ˆ˜์—…",
      "description": "์ œ๊ณต๋œ ์ •๋ณดยท์ž๋ฃŒ๋กœ ๊ฐ•์˜๊ณ„ํš์„œ ํ•ญ๋ชฉ ์ดˆ์•ˆ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค",
      "profile": "syllabusDraft"
    }
  ]
}

์ฃผ์š” Skill ID:

ID ์ด๋ฆ„ ์‘๋‹ต ํ˜•ํƒœ
chat ์ฑ—๋ด‡ JSON
syllabus-draft ์ˆ˜์—… SSE
evaluation-draft ํ‰๊ฐ€ SSE
archive-draft ๊ธฐ๋ก SSE
document-draft ๋ฌธ์„œ SSE
document-review ๋ฌธ์„œ ์ ๊ฒ€ SSE
form-response-draft ์‘๋‹ต SSE
activity-draft ํ™œ๋™ SSE
assessment-grade ์ฑ„์  SSE

form-response-draft์˜ docResponse(๊ธฐ์•ˆ๋ฌธ)๋Š” ์–‘์‹ ์ „์ฒด๋ฅผ ๋‹ค์‹œ ์“ฐ์ง€ ์•Š๊ณ  ์นธ๋งŒ ์ฑ„์›๋‹ˆ๋‹ค. ์–‘์‹์— (์ž‘์„ฑ) / (๋ณธ๋ฌธ ์ž‘์„ฑ) / (๊ธฐ์ž…) ๋“ฑ ์ž‘์„ฑ ์นธ์ด ์žˆ์œผ๋ฉด ํ•ด๋‹น ์นธ์— <<<SLOT>>> ์ฑ„์šฐ๊ธฐ๊ฐ€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค. ์ž‘์„ฑ ์นธ์ด ์—†์œผ๋ฉด ๋งˆํฌ๋‹ค์šด ๋นˆ ์…€ยทHTML ํ‘œ ๋นˆ td(์—๋””ํ„ฐ &nbsp;/๋นˆ p ํฌํ•จ)ยท๋ผ๋ฒจ:ยท๋ฐ‘์ค„ ๋“ฑ ๋นˆ์นธ์„ ์ถ”๋ก ํ•ด ๊ฐ™์€ SLOT ๊ฒฝ๋กœ๋กœ ์ฑ„์šฐ๋ฉฐ, ์ถ”๋ก ์ด ๋ถ€์กฑํ•˜๋ฉด ์–‘์‹์— (์ž‘์„ฑ)์„ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ์„ ํƒ ํ•„๋“œ ๋ˆ„๋ฝยท์ „์ฒด ์–‘์‹ ์žฌ์ž‘์„ฑ์€ ์„œ๋ฒ„์—์„œ 1ํšŒ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.


Alter ํ†ตํ•ฉ ํ„ด

ํŽ˜์ด์ง€ ๋ฌธ๋งฅ๊ณผ Skill์— ๋”ฐ๋ผ ์ดˆ์•ˆยท์ ๊ฒ€ยท์ผ๋ฐ˜ ๋Œ€ํ™”๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ดˆ์•ˆ/์ ๊ฒ€ Skill์€ SSE(text/event-stream), chat์€ JSON์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

POST /api/ai/alter

๊ถŒํ•œ: isLoggedIn (+ ํ•™๊ธฐ AI ์ ‘๊ทผ)

์š”์ฒญ ๋ณธ๋ฌธ

ํ•„๋“œ ํƒ€์ž… ํ•„์ˆ˜ ์„ค๋ช…
season string O ํ•™๊ธฐ ObjectId
message string X ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€
skill string X Skill ID. ์—†์œผ๋ฉด autoDetectSkill๋กœ ์ถ”์ •, ๊ธฐ๋ณธ chat
autoDetectSkill boolean X ๋ฉ”์‹œ์ง€์—์„œ Skill ์ž๋™ ๊ฐ์ง€ (๊ธฐ๋ณธ true)
context object X ํŽ˜์ด์ง€ ๋ฌธ๋งฅ(pageType, syllabusId, attachments ๋“ฑ)
history object[] X ์ง์ „ ๋Œ€ํ™” ์ด๋ ฅ
conversationId string X ๊ธฐ์กด ๋Œ€ํ™”์— ์ด์–ด์„œ ์ €์žฅ
persist boolean X ๋Œ€ํ™” ์ €์žฅ ์—ฌ๋ถ€ (๊ธฐ๋ณธ true)

์š”์ฒญ ์˜ˆ์‹œ (์ฑ—๋ด‡)

{
  "season": "507f1f77bcf86cd799439011",
  "skill": "chat",
  "message": "์ด๋ฒˆ ํ•™๊ธฐ ํ‰๊ฐ€ ์ผ์ •์€ ์–ด๋–ป๊ฒŒ ์žก์œผ๋ฉด ์ข‹์„๊นŒ์š”?",
  "context": { "pageType": "calendar" }
}

์‘๋‹ต (200, chat โ€” JSON)

{
  "skill": "chat",
  "message": "ํ‰๊ฐ€ ์ผ์ •์„ ์žก์„ ๋•Œโ€ฆ",
  "review": null,
  "draft": null,
  "conversationId": "507f1f77bcf86cd799439201"
}

ํŽ˜์ด์ง€ ์Šค๋ƒ…์ƒท ยท ๋ฐ์ดํ„ฐ ํ™•๋Œ€

ํ”„๋ก ํŠธ๋Š” ๋ฐ์ดํ„ฐ ํ™”๋ฉด์—์„œ registerPageContext๋กœ getChatSnapshot์„ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. chat Skill ์š”์ฒญ ์‹œ context.chatSnapshot๊ณผ context.dataExpand๊ฐ€ ํ•จ๊ป˜ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค.

๊ทœ์น™ ์„ค๋ช…
์ฝ๊ธฐ ๋ฌธ๋งฅ ํŽ˜์ด์ง€์— ์ด๋ฏธ ๋กœ๋“œยทํ‘œ์‹œ๋œ(๊ถŒํ•œ ํ†ต๊ณผ) ๋ฐ์ดํ„ฐ๋งŒ ์Šค๋ƒ…์ƒท์— ๋„ฃ์Šต๋‹ˆ๋‹ค. ์ถ”๊ฐ€ fetch ์—†์Œ
pageType: "sheet" ์–‘์‹ ์‘๋‹ต ์‹œํŠธ(ํ‘œยท์‹œ๊ฐ„ํ‘œยท๋ฌธ์„œ). UI ๋ผ๋ฒจใ€Œ์‘๋‹ต ๊ธฐ๋กใ€
Skill ์“ฐ๊ธฐ ์ดˆ์•ˆ ๋ฐ˜์˜ยท์ฑ„์  ๋“ฑ์€ ๋ณ„๋„ ํ›…(apply*Draft). ์Šค๋ƒ…์ƒท๋งŒ์œผ๋กœ๋Š” ์“ฐ์ง€ ์•Š์Œ

๋ฐ์ดํ„ฐ ํ™•๋Œ€ (context.dataExpand: true ๋˜๋Š” chatSnapshot.dataExpand: true): ์‚ฌ์šฉ์ž๊ฐ€ Alter ํŒจ๋„์—์„œ ์ผ  opt-in์ž…๋‹ˆ๋‹ค. ํ”„๋ก ํŠธยท๋ฐฑ์—”๋“œ ํ•œ๋„๋ฅผ ํ•จ๊ป˜ ์˜ฌ๋ฆฝ๋‹ˆ๋‹ค.

ํ•œ๋„ ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ํ™•๋Œ€
ํ•ญ๋ชฉ ์ˆ˜ 50 150
์Šค๋ƒ…์ƒท ์ „์ฒด ๊ธ€์ž 48,000 120,000
ํ•„๋“œ๊ฐ’ ๊ธ€์ž 40,000 40,000

์„œ๋ฒ„๋Š” buildAlterChatPageData์—์„œ ๋™์ผ ํ•œ๋„๋กœ ๋‹ค์‹œ ์ž๋ฆ…๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋งŒ ํ‚ค์›Œ๋„ ์„œ๋ฒ„ ํ•œ๋„๊ฐ€ ๋‚ฎ์œผ๋ฉด ์ž˜๋ฆฝ๋‹ˆ๋‹ค.
๋ชฉ๋กํ˜•์—์„œ ์ „์ฒด๋ณด๋‹ค ์ ๊ฒŒ ํฌํ•จ๋˜๋ฉด UIยทํ”„๋กฌํ”„ํŠธ์— ํฌํ•จ N / ์ „์ฒด M(๋˜๋Š” N/M๊ฑด)์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ๋ฌธ์„œ/์ฑ„์ ์ฒ˜๋Ÿผ ํ•ญ๋ชฉ 1๊ฑด ์Šค๋ƒ…์ƒท์˜ totalCount๋Š” ๋ฌธ์„œ ์ˆ˜(๋ณดํ†ต 1)์ด๋ฉฐ ํ•„๋“œ ์ˆ˜๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.

์‘๋‹ต (SSE, draft Skill)

์ด๋ฒคํŠธ๊ฐ€ ์ˆœ์ฐจ๋กœ ์ „์†ก๋ฉ๋‹ˆ๋‹ค.

์ด๋ฒคํŠธ ์„ค๋ช…
step ์ง„ํ–‰ ๋‹จ๊ณ„ ์•ˆ๋‚ด
done ์™„๋ฃŒ. skill, message, draft, review, conversationId
error ์˜ค๋ฅ˜. message, ์„ ํƒ์  conversationId
event: step
data: {"message":"์„ค์ • ํ™•์ธ ์ค‘..."}

event: done
data: {"skill":"syllabus-draft","message":"โ€ฆ","draft":{โ€ฆ},"review":null,"conversationId":"โ€ฆ"}

Alter ๋Œ€ํ™”

๋Œ€ํ™”๋Š” ํ•™๊ต ๋‹จ์œ„๋กœ ๋ชจ์ด๋ฉฐ, ํ•™๊ธฐ์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋ชฉ๋ก์— ๋‚˜ํƒ€๋‚  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
ํ•œ ๋Œ€ํ™”์—์„œ ์—ฌ๋Ÿฌ Skill์„ ์ด์–ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. lastSkill์€ ๋ชฉ๋ก ํ‘œ์‹œ์šฉ ์ตœ๊ทผ ์‹คํ–‰ Skill์ด๋ฉฐ, ๋Œ€ํ™”๊ฐ€ ๊ทธ Skill ์ „์šฉ์œผ๋กœ ์ž ๊ธฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
ํ™”๋ฉด ์ „์šฉ Skill(์‘๋‹ต ์ดˆ์•ˆยท์ฑ„์  ๋“ฑ)์€ ํ•ด๋‹น pageType ํ™”๋ฉด์—์„œ๋งŒ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ํŽ˜์ด์ง€๋ฅผ ๋– ๋‚˜๋ฉด ๊ทธ Skill ์‹คํ–‰์€ ๊ฑฐ์ ˆ๋˜๊ณ , pageType์ด ๋ฐ”๋€Œ๋ฉด ํ”„๋ก ํŠธ๋Š” ์ƒˆ ๋Œ€ํ™”๋กœ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ชฉ๋ก

GET /api/ai/alter/conversations?school=&season=&limit=

school์ด ์šฐ์„ ์ž…๋‹ˆ๋‹ค. season๋งŒ ์žˆ์œผ๋ฉด ํ•ด๋‹น ํ•™๊ธฐ์˜ ํ•™๊ต๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

์ƒ์„ฑ

POST /api/ai/alter/conversations
ํ•„๋“œ ํƒ€์ž… ํ•„์ˆ˜ ์„ค๋ช…
season string O ํ•™๊ธฐ ObjectId
title string X ๋Œ€ํ™” ์ œ๋ชฉ
pageType string X ์‹œ์ž‘ ํŽ˜์ด์ง€ ์œ ํ˜•
contextLabel string X ํ‘œ์‹œ์šฉ ๋ฌธ๋งฅ ๋ผ๋ฒจ
syllabusId string X ๊ด€๋ จ ๊ฐ•์˜๊ณ„ํš์„œ

๋ฉ”์‹œ์ง€ / ์ด๋ฆ„ ๋ณ€๊ฒฝ / ์‚ญ์ œ

GET    /api/ai/alter/conversations/:id/messages?limit=
PATCH  /api/ai/alter/conversations/:id   // body: { "title": "โ€ฆ" }
DELETE /api/ai/alter/conversations/:id   // ์†Œํ”„ํŠธ ์‚ญ์ œ โ†’ { "ok": true }
POST   /api/ai/alter/conversations/bulk-delete
  // body: { "ids": ["โ€ฆ"] }
  // โ†’ { "deleted": ["โ€ฆ"], "skipped": [{ "id": "โ€ฆ", "reason": "working"|"not_found" }] }

์ง„ํ–‰ ์ค‘์ธ(status=working) ๋Œ€ํ™”๋Š” ๋‹จ๊ฑดยท์ผ๊ด„ ๋ชจ๋‘ ์‚ญ์ œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


Skill ์ค€๋น„ ์„ค์ •

ํ•™๊ต ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌยทํ•™๊ธฐ ์„ค์ •์„ ๋ฐ˜์˜ํ•œ ์ง€์นจ/์ฐธ๊ณ ์ž๋ฃŒ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

GET /api/ai/alter/skill-settings?season=&skill=

๊ถŒํ•œ: isLoggedIn (+ ํ•™๊ธฐ AI ์ ‘๊ทผ)

skill ๊ธฐ๋ณธ๊ฐ’์€ chat์ž…๋‹ˆ๋‹ค. ์‘๋‹ต ๋ณธ๋ฌธ์€ ํ•ด์„๋œ ์ค€๋น„ ์„ค์ • ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค.


์ฒจ๋ถ€ ์—…๋กœ๋“œ

POST /api/ai/alter/attachment?season=
Content-Type: multipart/form-data
ํ•„๋“œ ํƒ€์ž… ํ•„์ˆ˜ ์„ค๋ช…
season query/body O ํ•™๊ธฐ ObjectId
file file O ์ฒจ๋ถ€ ํŒŒ์ผ

์ง€์›: txt / md / csv / pdf / docx / png / jpg / webp (์ตœ๋Œ€ 10MB).

์‘๋‹ต (200)

{
  "attachment": {
    "fileKey": "โ€ฆ",
    "originalName": "notes.pdf",
    "mimeType": "application/pdf",
    "extractedText": "โ€ฆ"
  }
}

๊ฐ•์˜๊ณ„ํš์„œ ๊ฒ€ํ†  (ํ•˜์œ„ ํ˜ธํ™˜)

POST /api/ai/alter์˜ skill=syllabus-draft์™€ ๋™์ผํ•œ SSE Skill์ž…๋‹ˆ๋‹ค.

POST /api/ai/syllabus/review

์š”์ฒญ: { "season", "context", "message" } โ€” ์‘๋‹ต์€ SSE(step / done / error).


ํ•™๊ธฐ ์ง€์นจ ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ

POST /api/ai/syllabus/guidelines-template

๊ถŒํ•œ: isAdManager (์•„์นด๋ฐ๋ฏธ admin / school manager)

์š”์ฒญ

{ "season": "507f1f77bcf86cd799439011" }

์‘๋‹ต (200)

{
  "guidelines": { },
  "usedFallback": false
}

์ƒ์„ฑ ์‹คํŒจ ์‹œ์—๋„ ๊ฒ€์ฆ๋œ ๊ธฐ๋ณธ ํ•œ๊ตญ์–ด ํ…œํ”Œ๋ฆฟ์„ usedFallback: true์™€ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


API ํ‚ค ํ…Œ์ŠคํŠธ (Owner)

POST /api/ai/test

๊ถŒํ•œ: isOwner

์š”์ฒญ

{
  "apiKey": "sk-โ€ฆ",
  "provider": "openai",
  "aiModel": "gpt-4o-mini"
}

์‘๋‹ต (200)

{
  "valid": true,
  "models": ["gpt-4o-mini", "gpt-4o"],
  "suggestedModel": "gpt-4o-mini"
}

ํ‚ค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด valid: false์™€ error ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค(HTTP 200์ธ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Œ).


๋ชจ๋ธ ๋ชฉ๋ก (Owner)

POST /api/ai/models

๊ถŒํ•œ: isOwner

ํ•„๋“œ ํƒ€์ž… ํ•„์ˆ˜ ์„ค๋ช…
apiKey string ์กฐ๊ฑด๋ถ€ ์ง์ ‘ ์ž…๋ ฅํ•œ ํ‚ค
provider string X ์ œ๊ณต์ž
academyId string ์กฐ๊ฑด๋ถ€ ์ €์žฅ๋œ ์•„์นด๋ฐ๋ฏธ ํ‚ค ์‚ฌ์šฉ ์‹œ

์‘๋‹ต (200)

{ "models": ["gpt-4o-mini", "gpt-4o"] }

๊ด€๋ จ ๋ฌธ์„œ