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 ์ ๊ทผ ๊ฒ์ฌ๋ฅผ ํต๊ณผํด์ผ ํฉ๋๋ค.
GET /api/ai/skills
๊ถํ: isLoggedIn
{
"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(์๋ํฐ /๋น p ํฌํจ)ยท๋ผ๋ฒจ:ยท๋ฐ์ค ๋ฑ ๋น์นธ์ ์ถ๋ก ํด ๊ฐ์ SLOT ๊ฒฝ๋ก๋ก ์ฑ์ฐ๋ฉฐ, ์ถ๋ก ์ด ๋ถ์กฑํ๋ฉด ์์์ (์์ฑ)์ ๋ช
์ํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค. ์ ํ ํ๋ ๋๋ฝยท์ ์ฒด ์์ ์ฌ์์ฑ์ ์๋ฒ์์ 1ํ ์ฌ์๋ํฉ๋๋ค.
ํ์ด์ง ๋ฌธ๋งฅ๊ณผ 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" }
}{
"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)์ด๋ฉฐ ํ๋ ์๊ฐ ์๋๋๋ค.
์ด๋ฒคํธ๊ฐ ์์ฐจ๋ก ์ ์ก๋ฉ๋๋ค.
| ์ด๋ฒคํธ | ์ค๋ช |
|---|---|
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":"โฆ"}
๋ํ๋ ํ๊ต ๋จ์๋ก ๋ชจ์ด๋ฉฐ, ํ๊ธฐ์ ๋ฌด๊ดํ๊ฒ ๋ชฉ๋ก์ ๋ํ๋ ์ ์์ต๋๋ค.
ํ ๋ํ์์ ์ฌ๋ฌ 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) ๋ํ๋ ๋จ๊ฑดยท์ผ๊ด ๋ชจ๋ ์ญ์ ๋์ง ์์ต๋๋ค.
ํ๊ต ๋ผ์ด๋ธ๋ฌ๋ฆฌยทํ๊ธฐ ์ค์ ์ ๋ฐ์ํ ์ง์นจ/์ฐธ๊ณ ์๋ฃ๋ฅผ ์กฐํํฉ๋๋ค.
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).
{
"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" }{
"guidelines": { },
"usedFallback": false
}์์ฑ ์คํจ ์์๋ ๊ฒ์ฆ๋ ๊ธฐ๋ณธ ํ๊ตญ์ด ํ
ํ๋ฆฟ์ usedFallback: true์ ํจ๊ป ๋ฐํํ ์ ์์ต๋๋ค.
POST /api/ai/test
๊ถํ: isOwner
{
"apiKey": "sk-โฆ",
"provider": "openai",
"aiModel": "gpt-4o-mini"
}{
"valid": true,
"models": ["gpt-4o-mini", "gpt-4o"],
"suggestedModel": "gpt-4o-mini"
}ํค๊ฐ ์ ํจํ์ง ์์ผ๋ฉด valid: false์ error ๋ฉ์์ง๋ฅผ ๋ฐํํฉ๋๋ค(HTTP 200์ธ ๊ฒฝ์ฐ๊ฐ ๋ง์).
POST /api/ai/models
๊ถํ: isOwner
| ํ๋ | ํ์ | ํ์ | ์ค๋ช |
|---|---|---|---|
apiKey |
string |
์กฐ๊ฑด๋ถ | ์ง์ ์ ๋ ฅํ ํค |
provider |
string |
X | ์ ๊ณต์ |
academyId |
string |
์กฐ๊ฑด๋ถ | ์ ์ฅ๋ ์์นด๋ฐ๋ฏธ ํค ์ฌ์ฉ ์ |
{ "models": ["gpt-4o-mini", "gpt-4o"] }- ์ฑํ API โ DM/๊ทธ๋ฃน/๋ณด๋ ์ฑํ (Alter์ ๋ณ๊ฐ)
- API ๊ฐ์
- ์ฌ์ฉ์ ๊ฐ์ด๋ โ Alter
- ์์นด๋ฐ๋ฏธ ๊ด๋ฆฌ โ AI ์ค์