From 3f6e0050265633a697824f0d6020d4be3e4de0d5 Mon Sep 17 00:00:00 2001 From: cherry2250 Date: Fri, 24 Oct 2025 10:10:16 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=20=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 102 + design/.DS_Store | Bin 0 -> 6148 bytes design/aidata/๋งค์žฅ_ํ”„๋กœํ•„_๋ฐ์ดํ„ฐ.md | 1938 +++++++++++++ design/aidata/์‹œ์ฆŒ๋ณ„_์ด๋ฒคํŠธ_์„ฑ๊ณผ_๋ฐ์ดํ„ฐ.md | 564 ++++ design/aidata/์—…์ข…๋ณ„_์„ฑ๊ณต_์ด๋ฒคํŠธ_๋ฐ์ดํ„ฐ.md | 982 +++++++ design/aidata/์ง€์—ญ๋ณ„_ํŠธ๋ Œ๋“œ_๋ฐ์ดํ„ฐ.md | 955 +++++++ design/backend/api/API-์„ค๊ณ„์„œ.md | 665 +++++ design/backend/api/API_CONVENTION.md | 914 ++++++ design/backend/api/ai-service-api.yaml | 849 ++++++ design/backend/api/analytics-service-api.yaml | 1081 +++++++ design/backend/api/content-service-api.yaml | 1158 ++++++++ .../backend/api/distribution-service-api.yaml | 651 +++++ design/backend/api/event-service-api.yaml | 1384 +++++++++ .../api/participation-service-api.yaml | 820 ++++++ design/backend/api/user-service-api.yaml | 991 +++++++ .../backend/logical/logical-architecture.md | 869 ++++++ .../backend/logical/logical-architecture.mmd | 71 + design/backend/sequence/inner/README.md | 393 +++ .../sequence/inner/ai-ํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ถ”์ฒœ.puml | 343 +++ .../inner/analytics-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml | 342 +++ .../inner/analytics-๋ฐฐํฌ์™„๋ฃŒ๊ตฌ๋….puml | 168 ++ .../inner/analytics-์ด๋ฒคํŠธ์ƒ์„ฑ๊ตฌ๋….puml | 134 + .../inner/analytics-์ฐธ์—ฌ์ž๋“ฑ๋ก๊ตฌ๋….puml | 135 + .../inner/content-์ด๋ฏธ์ง€๊ฒฐ๊ณผ์กฐํšŒ.puml | 140 + .../sequence/inner/content-์ด๋ฏธ์ง€์ƒ์„ฑ.puml | 255 ++ .../inner/content-์ด๋ฏธ์ง€์ƒ์„ฑ์š”์ฒญ.puml | 90 + .../inner/distribution-๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ.puml | 141 + .../sequence/inner/event-AI์ถ”์ฒœ์š”์ฒญ.puml | 126 + .../sequence/inner/event-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml | 73 + .../sequence/inner/event-๋ชฉ๋ก์กฐํšŒ.puml | 64 + .../sequence/inner/event-๋ชฉ์ ์„ ํƒ.puml | 110 + .../sequence/inner/event-์ƒ์„ธ์กฐํšŒ.puml | 54 + .../sequence/inner/event-์ตœ์ข…์Šน์ธ๋ฐ๋ฐฐํฌ.puml | 157 ++ .../sequence/inner/event-์ถ”์ฒœ๊ฒฐ๊ณผ์กฐํšŒ.puml | 140 + .../sequence/inner/event-์ถ”์ฒœ์•ˆ์„ ํƒ.puml | 116 + .../sequence/inner/event-์ฝ˜ํ…์ธ ์„ ํƒ.puml | 118 + .../inner/participation-๋‹น์ฒจ์ž์ถ”์ฒจ.puml | 164 ++ .../inner/participation-์ด๋ฒคํŠธ์ฐธ์—ฌ.puml | 129 + .../inner/participation-์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ.puml | 120 + .../backend/sequence/inner/user-๋กœ๊ทธ์•„์›ƒ.puml | 155 ++ .../backend/sequence/inner/user-๋กœ๊ทธ์ธ.puml | 147 + .../sequence/inner/user-ํ”„๋กœํ•„์ˆ˜์ •.puml | 233 ++ .../backend/sequence/inner/user-ํšŒ์›๊ฐ€์ž….puml | 149 + design/backend/sequence/outer/README.md | 304 ++ .../sequence/outer/๊ณ ๊ฐ์ฐธ์—ฌํ”Œ๋กœ์šฐ.puml | 164 ++ .../sequence/outer/์‚ฌ์šฉ์ž์ธ์ฆํ”Œ๋กœ์šฐ.puml | 177 ++ .../sequence/outer/์„ฑ๊ณผ๋ถ„์„ํ”Œ๋กœ์šฐ.puml | 198 ++ .../sequence/outer/์ด๋ฒคํŠธ์ƒ์„ฑํ”Œ๋กœ์šฐ.puml | 250 ++ design/pattern/architecture-pattern.md | 619 +++++ .../backup/architecture-pattern-backup.md | 1097 ++++++++ .../backup/architecture-pattern-backup2.md | 1165 ++++++++ design/uiux/prototype/01-๋กœ๊ทธ์ธ.html | 196 ++ design/uiux/prototype/02-ํšŒ์›๊ฐ€์ž….html | 400 +++ design/uiux/prototype/03-ํ”„๋กœํ•„.html | 214 ++ design/uiux/prototype/04-๋กœ๊ทธ์•„์›ƒํ™•์ธ.html | 54 + design/uiux/prototype/05-๋Œ€์‹œ๋ณด๋“œ.html | 219 ++ design/uiux/prototype/06-์ด๋ฒคํŠธ๋ชฉ๋ก.html | 323 +++ design/uiux/prototype/07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ.html | 216 ++ design/uiux/prototype/08-AI์ด๋ฒคํŠธ์ถ”์ฒœ.html | 473 ++++ design/uiux/prototype/09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ.html | 447 +++ design/uiux/prototype/10-์ฝ˜ํ…์ธ ํŽธ์ง‘.html | 209 ++ design/uiux/prototype/11-๋ฐฐํฌ์ฑ„๋„์„ ํƒ.html | 421 +++ design/uiux/prototype/12-์ตœ์ข…์Šน์ธ.html | 367 +++ design/uiux/prototype/13-์ด๋ฒคํŠธ์ƒ์„ธ.html | 437 +++ design/uiux/prototype/14-์ฐธ์—ฌ์ž๋ชฉ๋ก.html | 400 +++ design/uiux/prototype/15-์ด๋ฒคํŠธ์ฐธ์—ฌ.html | 309 ++ design/uiux/prototype/16-๋‹น์ฒจ์ž์ถ”์ฒจ.html | 536 ++++ design/uiux/prototype/17-์„ฑ๊ณผ๋ถ„์„.html | 430 +++ design/uiux/prototype/common.js | 1071 +++++++ design/uiux/prototype/styles.css | 973 +++++++ design/uiux/style-guide.md | 1554 +++++++++++ design/uiux/uiux-design.md | 1002 +++++++ design/uiux/uiux.md | 2473 +++++++++++++++++ design/userstory-table.md | 344 +++ design/userstory.md | 998 +++++++ design/๊ตฌํ˜„๋ฐฉ์•ˆ-AI์ด๋ฒคํŠธ์„ค๊ณ„.md | 1612 +++++++++++ 76 files changed, 37842 insertions(+) create mode 100644 CLAUDE.md create mode 100644 design/.DS_Store create mode 100644 design/aidata/๋งค์žฅ_ํ”„๋กœํ•„_๋ฐ์ดํ„ฐ.md create mode 100644 design/aidata/์‹œ์ฆŒ๋ณ„_์ด๋ฒคํŠธ_์„ฑ๊ณผ_๋ฐ์ดํ„ฐ.md create mode 100644 design/aidata/์—…์ข…๋ณ„_์„ฑ๊ณต_์ด๋ฒคํŠธ_๋ฐ์ดํ„ฐ.md create mode 100644 design/aidata/์ง€์—ญ๋ณ„_ํŠธ๋ Œ๋“œ_๋ฐ์ดํ„ฐ.md create mode 100644 design/backend/api/API-์„ค๊ณ„์„œ.md create mode 100644 design/backend/api/API_CONVENTION.md create mode 100644 design/backend/api/ai-service-api.yaml create mode 100644 design/backend/api/analytics-service-api.yaml create mode 100644 design/backend/api/content-service-api.yaml create mode 100644 design/backend/api/distribution-service-api.yaml create mode 100644 design/backend/api/event-service-api.yaml create mode 100644 design/backend/api/participation-service-api.yaml create mode 100644 design/backend/api/user-service-api.yaml create mode 100644 design/backend/logical/logical-architecture.md create mode 100644 design/backend/logical/logical-architecture.mmd create mode 100644 design/backend/sequence/inner/README.md create mode 100644 design/backend/sequence/inner/ai-ํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ถ”์ฒœ.puml create mode 100644 design/backend/sequence/inner/analytics-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml create mode 100644 design/backend/sequence/inner/analytics-๋ฐฐํฌ์™„๋ฃŒ๊ตฌ๋….puml create mode 100644 design/backend/sequence/inner/analytics-์ด๋ฒคํŠธ์ƒ์„ฑ๊ตฌ๋….puml create mode 100644 design/backend/sequence/inner/analytics-์ฐธ์—ฌ์ž๋“ฑ๋ก๊ตฌ๋….puml create mode 100644 design/backend/sequence/inner/content-์ด๋ฏธ์ง€๊ฒฐ๊ณผ์กฐํšŒ.puml create mode 100644 design/backend/sequence/inner/content-์ด๋ฏธ์ง€์ƒ์„ฑ.puml create mode 100644 design/backend/sequence/inner/content-์ด๋ฏธ์ง€์ƒ์„ฑ์š”์ฒญ.puml create mode 100644 design/backend/sequence/inner/distribution-๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ.puml create mode 100644 design/backend/sequence/inner/event-AI์ถ”์ฒœ์š”์ฒญ.puml create mode 100644 design/backend/sequence/inner/event-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml create mode 100644 design/backend/sequence/inner/event-๋ชฉ๋ก์กฐํšŒ.puml create mode 100644 design/backend/sequence/inner/event-๋ชฉ์ ์„ ํƒ.puml create mode 100644 design/backend/sequence/inner/event-์ƒ์„ธ์กฐํšŒ.puml create mode 100644 design/backend/sequence/inner/event-์ตœ์ข…์Šน์ธ๋ฐ๋ฐฐํฌ.puml create mode 100644 design/backend/sequence/inner/event-์ถ”์ฒœ๊ฒฐ๊ณผ์กฐํšŒ.puml create mode 100644 design/backend/sequence/inner/event-์ถ”์ฒœ์•ˆ์„ ํƒ.puml create mode 100644 design/backend/sequence/inner/event-์ฝ˜ํ…์ธ ์„ ํƒ.puml create mode 100644 design/backend/sequence/inner/participation-๋‹น์ฒจ์ž์ถ”์ฒจ.puml create mode 100644 design/backend/sequence/inner/participation-์ด๋ฒคํŠธ์ฐธ์—ฌ.puml create mode 100644 design/backend/sequence/inner/participation-์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ.puml create mode 100644 design/backend/sequence/inner/user-๋กœ๊ทธ์•„์›ƒ.puml create mode 100644 design/backend/sequence/inner/user-๋กœ๊ทธ์ธ.puml create mode 100644 design/backend/sequence/inner/user-ํ”„๋กœํ•„์ˆ˜์ •.puml create mode 100644 design/backend/sequence/inner/user-ํšŒ์›๊ฐ€์ž….puml create mode 100644 design/backend/sequence/outer/README.md create mode 100644 design/backend/sequence/outer/๊ณ ๊ฐ์ฐธ์—ฌํ”Œ๋กœ์šฐ.puml create mode 100644 design/backend/sequence/outer/์‚ฌ์šฉ์ž์ธ์ฆํ”Œ๋กœ์šฐ.puml create mode 100644 design/backend/sequence/outer/์„ฑ๊ณผ๋ถ„์„ํ”Œ๋กœ์šฐ.puml create mode 100644 design/backend/sequence/outer/์ด๋ฒคํŠธ์ƒ์„ฑํ”Œ๋กœ์šฐ.puml create mode 100644 design/pattern/architecture-pattern.md create mode 100644 design/pattern/backup/architecture-pattern-backup.md create mode 100644 design/pattern/backup/architecture-pattern-backup2.md create mode 100644 design/uiux/prototype/01-๋กœ๊ทธ์ธ.html create mode 100644 design/uiux/prototype/02-ํšŒ์›๊ฐ€์ž….html create mode 100644 design/uiux/prototype/03-ํ”„๋กœํ•„.html create mode 100644 design/uiux/prototype/04-๋กœ๊ทธ์•„์›ƒํ™•์ธ.html create mode 100644 design/uiux/prototype/05-๋Œ€์‹œ๋ณด๋“œ.html create mode 100644 design/uiux/prototype/06-์ด๋ฒคํŠธ๋ชฉ๋ก.html create mode 100644 design/uiux/prototype/07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ.html create mode 100644 design/uiux/prototype/08-AI์ด๋ฒคํŠธ์ถ”์ฒœ.html create mode 100644 design/uiux/prototype/09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ.html create mode 100644 design/uiux/prototype/10-์ฝ˜ํ…์ธ ํŽธ์ง‘.html create mode 100644 design/uiux/prototype/11-๋ฐฐํฌ์ฑ„๋„์„ ํƒ.html create mode 100644 design/uiux/prototype/12-์ตœ์ข…์Šน์ธ.html create mode 100644 design/uiux/prototype/13-์ด๋ฒคํŠธ์ƒ์„ธ.html create mode 100644 design/uiux/prototype/14-์ฐธ์—ฌ์ž๋ชฉ๋ก.html create mode 100644 design/uiux/prototype/15-์ด๋ฒคํŠธ์ฐธ์—ฌ.html create mode 100644 design/uiux/prototype/16-๋‹น์ฒจ์ž์ถ”์ฒจ.html create mode 100644 design/uiux/prototype/17-์„ฑ๊ณผ๋ถ„์„.html create mode 100644 design/uiux/prototype/common.js create mode 100644 design/uiux/prototype/styles.css create mode 100644 design/uiux/style-guide.md create mode 100644 design/uiux/uiux-design.md create mode 100644 design/uiux/uiux.md create mode 100644 design/userstory-table.md create mode 100644 design/userstory.md create mode 100644 design/๊ตฌํ˜„๋ฐฉ์•ˆ-AI์ด๋ฒคํŠธ์„ค๊ณ„.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ba319b4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,102 @@ +# ํ”„๋ก ํŠธ์—”๋“œ ๊ฐ€์ด๋“œ + +[Git ์—ฐ๋™] + +- "pull" ๋ช…๋ น์–ด ์ž…๋ ฅ ์‹œ Git pull ๋ช…๋ น์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ์ถฉ๋Œ์ด ์žˆ์„ ๋•Œ ์ตœ์‹  ํŒŒ์ผ๋กœ ๋ณ‘ํ•ฉ ์ˆ˜ํ–‰ +- "push" ๋˜๋Š” "ํ‘ธ์‹œ" ๋ช…๋ น์–ด ์ž…๋ ฅ ์‹œ git add, commit, push๋ฅผ ์ˆ˜ํ–‰ +- Commit Message๋Š” ํ•œ๊ธ€๋กœ ํ•จ + +[URL๋งํฌ ์ฐธ์กฐ] + +- URL๋งํฌ๋Š” WebFetch๊ฐ€ ์•„๋‹Œ 'curl {URL} > claude/{filename}'๋ช…๋ น์œผ๋กœ ์ €์žฅ +- ๋™์ผํ•œ ํŒŒ์ผ์ด ์žˆ์œผ๋ฉด ๋ฎ์–ด ์”€ +- 'claude'๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑํ•˜๊ณ  ๋‹ค์šด๋กœ๋“œ +- ์ €์žฅ๋œ ํŒŒ์ผ์„ ์ฝ์–ด ์‚ฌ์šฉํ•จ + +## ์‚ฐ์ถœ๋ฌผ ๋””๋ ‰ํ† ๋ฆฌ + +- ํ”„๋กœํ† ํƒ€์ž…: design/prototype/\* +- API๋ช…์„ธ์„œ: design/api/\*.json +- UI/UX์„ค๊ณ„์„œ: design/frontend/uiux-design.md +- ์Šคํƒ€์ผ๊ฐ€์ด๋“œ: design/frontend/style-guide.md +- ์ •๋ณด์•„ํ‚คํ…์ฒ˜: design/frontend/ia.md +- API๋งคํ•‘์„ค๊ณ„์„œ: design/frontend/api-mapping.md +- ์œ ์ €์Šคํ† ๋ฆฌ: design/userstory.md + +## ๊ฐ€์ด๋“œ + +- ํ”„๋ก ํŠธ์—”๋“œ์„ค๊ณ„๊ฐ€์ด๋“œ + - ์„ค๋ช…: ํ”„๋ก ํŠธ์—”๋“œ ์„ค๊ณ„ ๋ฐฉ๋ฒ• ์•ˆ๋‚ด + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/design/frontend-design.md + - ํŒŒ์ผ๋ช…: frontend-design.md +- ํ”„๋ก ํŠธ์—”๋“œ๊ฐœ๋ฐœ๊ฐ€์ด๋“œ + - ์„ค๋ช…: ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/develop/dev-frontend.md + - ํŒŒ์ผ๋ช…: dev-frontend.md +- ํ”„๋ก ํŠธ์—”๋“œ์ปจํ…Œ์ด๋„ˆ์ด๋ฏธ์ง€์ž‘์„ฑ๊ฐ€์ด๋“œ + - ์„ค๋ช…: ํ”„๋ก ํŠธ์—”๋“œ ์ปจํ…Œ์ด๋„ˆ ์ด๋ฏธ์ง€ ์ž‘์„ฑ ๊ฐ€์ด๋“œ + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/deploy/build-image-front.md + - ํŒŒ์ผ๋ช…: build-image-front.md +- ํ”„๋ก ํŠธ์—”๋“œ์ปจํ…Œ์ด๋„ˆ์‹คํ–‰๋ฐฉ๋ฒ•๊ฐ€์ด๋“œ + - ์„ค๋ช…: ํ”„๋ก ํŠธ์—”๋“œ ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰๋ฐฉ๋ฒ• ๊ฐ€์ด๋“œ + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/deploy/run-container-guide-front.md + - ํŒŒ์ผ๋ช…: run-container-guide-front.md +- ํ”„๋ก ํŠธ์—”๋“œ๋ฐฐํฌ๊ฐ€์ด๋“œ + - ์„ค๋ช…: ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋น„์Šค๋ฅผ ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค ํด๋Ÿฌ์Šคํ„ฐ์— ๋ฐฐํฌํ•˜๋Š” ๊ฐ€์ด๋“œ + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/deploy/deploy-k8s-front.md + - ํŒŒ์ผ๋ช…: deploy-k8s-front.md +- ํ”„๋ก ํŠธ์—”๋“œJenkinsํŒŒ์ดํ”„๋ผ์ธ์ž‘์„ฑ๊ฐ€์ด๋“œ + - ์„ค๋ช…: ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋น„์Šค๋ฅผ Jenkins๋ฅผ ์ด์šฉํ•˜์—ฌ CI/CDํ•˜๋Š” ๋ฐฐํฌ ๊ฐ€์ด๋“œ + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/deploy/deploy-jenkins-cicd-front.md + - ํŒŒ์ผ๋ช…: deploy-jenkins-cicd-front.md +- ํ”„๋ก ํŠธ์—”๋“œGitHubActionsํŒŒ์ดํ”„๋ผ์ธ์ž‘์„ฑ๊ฐ€์ด๋“œ + - ์„ค๋ช…: ํ”„๋ก ํŠธ์—”๋“œ ์„œ๋น„์Šค๋ฅผ GitHub Actions๋ฅผ ์ด์šฉํ•˜์—ฌ CI/CDํ•˜๋Š” ๋ฐฐํฌ ๊ฐ€์ด๋“œ + - URL: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/guides/deploy/deploy-actions-cicd-front.md + - ํŒŒ์ผ๋ช…: deploy-actions-cicd-front.md + +## ํ”„๋กฌํ”„ํŠธ ์•ฝ์–ด + +### ์—ญํ•  ์•ฝ์–ด + +- "@front": "--persona-front" +- "@devops": "--persona-devops" + +### ์ž‘์—… ์•ฝ์–ด + +- "@complex-flag": --seq --c7 --uc --wave-mode auto --wave-strategy systematic --delegate auto + +- "@plan": --plan --think +- "@dev-front": /sc:implement @front --think-hard @complex-flag +- "@cicd": /sc:implement @devops --think @complex-flag +- "@document": /sc:document --think @scribe @complex-flag +- "@fix": /sc:troubleshoot --think @complex-flag +- "@estimate": /sc:estimate --think-hard @complex-flag +- "@improve": /sc:improve --think @complex-flag +- "@analyze": /sc:analyze --think --seq +- "@explain": /sc:explain --think --seq --answer-only + +## Lessons Learned + +**ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ ์ ˆ์ฐจ**: + +- ๊ฐœ๋ฐœ๊ฐ€์ด๋“œ์˜ "6. ๊ฐ ํŽ˜์ด์ง€๋ณ„ ๊ตฌํ˜„" ๋‹จ๊ณ„์—์„œ๋Š” ๋นŒ๋“œ ๋ฐ ์—๋Ÿฌ ํ•ด๊ฒฐ๊นŒ์ง€๋งŒ ์ˆ˜ํ–‰ +- ๊ฐœ๋ฐœ์„œ๋ฒ„(`npm run dev`) ์‹คํ–‰์€ ํ•ญ์ƒ ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ˆ˜ํ–‰ +- ๊ฐœ๋ฐœ์ž๋Š” ๋นŒ๋“œ(`npm run build`) ์„ฑ๊ณต๊นŒ์ง€๋งŒ ํ™•์ธํ•˜๊ณ  ์„œ๋ฒ„ ์‹คํ–‰์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์š”์ฒญ +- ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž„์˜๋กœ ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š๊ณ  ์‚ฌ์šฉ์ž ํ™•์ธ ํ›„ ์ง„ํ–‰ + +**ํ”„๋กœํ† ํƒ€์ž… ๋ถ„์„ ๋ฐ ํ…Œ์ŠคํŠธ**: + +- ํ”„๋กœํ† ํƒ€์ž… HTML ํŒŒ์ผ์€ ๋ฐ˜๋“œ์‹œ Playwright MCP๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋ฐ”์ผ ํ™”๋ฉด(375x812)์—์„œ ํ™•์ธ +- ํ”„๋กœํ† ํƒ€์ž…์˜ ๋ชจ๋“  ์ธํ„ฐ๋ž™์…˜๊ณผ ์•ก์…˜์„ ์‹ค์ œ๋กœ ํด๋ฆญํ•˜์—ฌ ๋™์ž‘ ํ™•์ธ ํ•„์š” + +**์„œ๋น„์Šค ์žฌ๋ฐฐํฌ ๊ฐ€์ด๋“œ** +์„œ๋น„์Šค ์ˆ˜์ • ํ›„ ์žฌ๋ฐฐํฌ ์‹œ ๋‹ค์Œ ์ ˆ์ฐจ๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค: + +1. ์ด๋ฏธ์ง€ ๋นŒ๋“œ: deployment/container/build-image.md ์ฐธ์กฐํ•˜์—ฌ ๋นŒ๋“œ +2. ์ด๋ฏธ์ง€๋ฅผ ACRํ˜•์‹์œผ๋กœ ํƒœ๊น… +3. ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰: deployment/container/run-container-guide.md์˜ '8. ์žฌ๋ฐฐํฌ ๋ฐฉ๋ฒ•' ์ฐธ์กฐํ•˜์—ฌ ์‹คํ–‰ + - ์ปจํ…Œ์ด๋„ˆ ์ค‘๋‹จ + - ์ด๋ฏธ์ง€ ์‚ญ์ œ + - ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ + +- ํ…Œ์ŠคํŠธ๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์š”์ฒญ diff --git a/design/.DS_Store b/design/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e1d54c9ab92ff802b04b22537df761ae2dd7079a GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8-BN@c6nb3nTCk=f7B8XJ7cim+m70*E!I&*gV}(-4SzpK}@p+ut z-H63{6|plg`_1gk>}Eg6{xHUPKachqa~NX+6p^E`La=*fsA7{5IgSy|vT>NG5d1eA z`|E(;USm0nL7%^Ve>hI!Y})C(^Gdb0u~`>&(G)lSy_)&cV47$BU~+}txl&13+Cg{` zk7h$>`$VPFAWlaUT@XhjNV&d@(@4$wDo>+K*ZL+P8lo|DTJyPkerN>ctSsfp0Ba7E5>q<8wVv{v=IQdIx=Fewjx|3=jjv05Pz> z4A?V4G}o5`S~)R54E(?V?hg_a(X&_>)LREMczwor4G{%wd`looi=M^8AVfg8DFrm8 z+&(e5DTjV(^E``%K~v7So*BlmGnbDSu4jjSsnZ$v3{p!B5Ch8$RE_K4`F{$(%*sdp zatV3F05R~-7~r*mKj_1v?AiKld3e@J&|6Rx%q!3U0ek5Z01oaWEgdv*i8kbU77K$o T3ij)AK)MKMLZ~4Ieu04x!W2rD literal 0 HcmV?d00001 diff --git a/design/aidata/๋งค์žฅ_ํ”„๋กœํ•„_๋ฐ์ดํ„ฐ.md b/design/aidata/๋งค์žฅ_ํ”„๋กœํ•„_๋ฐ์ดํ„ฐ.md new file mode 100644 index 0000000..dfd1d4d --- /dev/null +++ b/design/aidata/๋งค์žฅ_ํ”„๋กœํ•„_๋ฐ์ดํ„ฐ.md @@ -0,0 +1,1938 @@ +# ๋งค์žฅ ํ”„๋กœํ•„ ๋ฐ์ดํ„ฐ ์ƒ˜ํ”Œ + +**์ž‘์„ฑ์ผ**: 2025-10-21 +**๋ฒ„์ „**: 1.0 +**๋ชฉ์ **: AI ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค ํ•™์Šต ๋ฐ์ดํ„ฐ + +--- + +## ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ + +๊ฐ ๋งค์žฅ ํ”„๋กœํ•„์€ ๋‹ค์Œ ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค: + +- **๊ธฐ๋ณธ์ •๋ณด**: ๋งค์žฅID, ๋งค์žฅ๋ช…, ์—…์ข…, ์ง€์—ญ, ์ฃผ์†Œ, ์˜์—…์‹œ๊ฐ„ +- **์šด์˜์ •๋ณด**: ์ง์› ์ˆ˜, ์ขŒ์„ ์ˆ˜, ์›” ํ‰๊ท  ๋งค์ถœ, ๊ฐ๋‹จ๊ฐ€ +- **๋ฉ”๋‰ด์ •๋ณด**: ์ฃผ์š” ๋ฉ”๋‰ด ๋ฐ ๊ฐ€๊ฒฉ +- **ํƒ€๊ฒŸ์ •๋ณด**: ์ฃผ์š” ๊ณ ๊ฐ์ธต, ์ƒ๊ถŒ ํŠน์„ฑ +- **ํŠน์„ฑ**: ๊ฐ•์  ๋ฐ ๊ฒฝ์Ÿ๋ ฅ + +--- + +## 1. Korean Restaurant (ํ•œ๊ตญ ์Œ์‹์ ) + +### 1-1. ์ •๊ฐ€๋„คํ•œ์ •์‹ +```json +{ + "storeId": "KR001", + "storeName": "์ •๊ฐ€๋„คํ•œ์ •์‹", + "category": "korean_restaurant", + "categoryKo": "ํ•œ๊ตญ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "๊ฐ•๋‚จ๊ตฌ", + "address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-21:00", + "holiday": "๋งค์ฃผ ์ผ์š”์ผ ํœด๋ฌด" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 45000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "ํ•œ์ •์‹ A", "price": 15000}, + {"name": "ํ•œ์ •์‹ B", "price": 20000}, + {"name": "๋ถˆ๊ณ ๊ธฐ์ •์‹", "price": 12000}, + {"name": "๊ฐˆ๋น„ํƒ•", "price": 13000} + ], + "targetCustomers": "์ง์žฅ์ธ, 40-60๋Œ€, ๊ฐ€์กฑ ๋ชจ์ž„", + "strongPoints": "์ „ํ†ต ํ•œ์ •์‹, ์ •๊ฐˆํ•œ ์ƒ์ฐจ๋ฆผ, ์ ์‹ฌ ํŠน์„  ๋ฉ”๋‰ด", + "locationCharacter": "๊ฐ•๋‚จ ์—…๋ฌด์ง€๊ตฌ, ์ ์‹ฌ ์ˆ˜์š” ๋†’์Œ" +} +``` + +### 1-2. ์šฐ๋ฆฌ์ง‘๋ฐฅ์ƒ +```json +{ + "storeId": "KR002", + "storeName": "์šฐ๋ฆฌ์ง‘๋ฐฅ์ƒ", + "category": "korean_restaurant", + "categoryKo": "ํ•œ๊ตญ ์Œ์‹์ ", + "location": { + "city": "๋ถ€์‚ฐ", + "district": "ํ•ด์šด๋Œ€๊ตฌ", + "address": "๋ถ€์‚ฐ์‹œ ํ•ด์šด๋Œ€๊ตฌ ์šฐ๋™ 456" + }, + "businessHours": { + "weekday": "10:00-21:00", + "weekend": "10:00-21:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 3, + "seats": 25, + "monthlyRevenue": 28000000, + "avgCheckSize": 11000, + "menu": [ + {"name": "๊น€์น˜์ฐŒ๊ฐœ", "price": 9000}, + {"name": "๋œ์žฅ์ฐŒ๊ฐœ", "price": 9000}, + {"name": "์ œ์œก๋ณถ์Œ", "price": 10000}, + {"name": "์ƒ์„ ๊ตฌ์ด", "price": 12000} + ], + "targetCustomers": "์ง€์—ญ ์ฃผ๋ฏผ, 30-50๋Œ€, 1์ธ ์‹์‚ฌ ๊ณ ๊ฐ", + "strongPoints": "๊ฐ€์„ฑ๋น„, ๋„‰๋„‰ํ•œ ๋ฐ˜์ฐฌ, ๋น ๋ฅธ ์„œ๋น™", + "locationCharacter": "์ฃผํƒ๊ฐ€ ์ƒ๊ถŒ, ๋‹จ๊ณจ ๊ณ ๊ฐ ๋งŽ์Œ" +} +``` + +### 1-3. ์ „์ฃผ๋ณธ๊ฐ€ +```json +{ + "storeId": "KR003", + "storeName": "์ „์ฃผ๋ณธ๊ฐ€", + "category": "korean_restaurant", + "categoryKo": "ํ•œ๊ตญ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "์ค‘๊ตฌ", + "address": "์„œ์šธ์‹œ ์ค‘๊ตฌ ๋ช…๋™๊ธธ 789" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 8, + "seats": 60, + "monthlyRevenue": 72000000, + "avgCheckSize": 24000, + "menu": [ + {"name": "์ „์ฃผ๋น„๋น”๋ฐฅ", "price": 13000}, + {"name": "๋Œ์†ฅ๋น„๋น”๋ฐฅ", "price": 15000}, + {"name": "์ฝฉ๋‚˜๋ฌผ๊ตญ๋ฐฅ", "price": 10000}, + {"name": "์œกํšŒ๋น„๋น”๋ฐฅ", "price": 18000} + ], + "targetCustomers": "๊ด€๊ด‘๊ฐ, ์™ธ๊ตญ์ธ, ์ „ ์—ฐ๋ น๋Œ€", + "strongPoints": "์ „ํ†ต ๋น„๋น”๋ฐฅ ์ „๋ฌธ์ , ๊ด€๊ด‘๋ช…์†Œ ์œ„์น˜, ์™ธ๊ตญ์–ด ๋ฉ”๋‰ด ์ œ๊ณต", + "locationCharacter": "๋ช…๋™ ๊ด€๊ด‘์ง€, ์ฃผ๋ง/๊ณตํœด์ผ ๋งค์ถœ ๋†’์Œ" +} +``` + +--- + +## 2. Cafe (์นดํŽ˜) + +### 2-1. ๊ฐ์„ฑ๊ณต๊ฐ„ +```json +{ + "storeId": "CF001", + "storeName": "๊ฐ์„ฑ๊ณต๊ฐ„", + "category": "cafe", + "categoryKo": "์นดํŽ˜", + "location": { + "city": "์„œ์šธ", + "district": "์„ฑ์ˆ˜๋™", + "address": "์„œ์šธ์‹œ ์„ฑ๋™๊ตฌ ์„ฑ์ˆ˜์ด๋กœ 234" + }, + "businessHours": { + "weekday": "09:00-22:00", + "weekend": "10:00-23:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 4, + "seats": 45, + "monthlyRevenue": 38000000, + "avgCheckSize": 8500, + "menu": [ + {"name": "์•„๋ฉ”๋ฆฌ์นด๋…ธ", "price": 4500}, + {"name": "์นดํŽ˜๋ผ๋–ผ", "price": 5000}, + {"name": "ํฌ๋ฃจ์•„์ƒ", "price": 4000}, + {"name": "์‹œ๊ทธ๋‹ˆ์ฒ˜ ์ผ€์ดํฌ", "price": 7500} + ], + "targetCustomers": "20-30๋Œ€ ์—ฌ์„ฑ, ์ธ์Šคํƒ€๊ทธ๋ž˜๋จธ, ํ”„๋ฆฌ๋žœ์„œ", + "strongPoints": "๊ฐ์„ฑ ์ธํ…Œ๋ฆฌ์–ด, ํฌํ† ์กด, SNS ๋งˆ์ผ€ํŒ… ๊ฐ•์ ", + "locationCharacter": "์„ฑ์ˆ˜๋™ ํ•ซํ”Œ๋ ˆ์ด์Šค, ์ฃผ๋ง ๋ฐฉ๋ฌธ๊ฐ ๋งŽ์Œ" +} +``` + +### 2-2. ๋ถ์นดํŽ˜ ์ฑ…๊ทธ๋ฆฌ๊ณ  +```json +{ + "storeId": "CF002", + "storeName": "๋ถ์นดํŽ˜ ์ฑ…๊ทธ๋ฆฌ๊ณ ", + "category": "cafe", + "categoryKo": "์นดํŽ˜", + "location": { + "city": "๋Œ€์ „", + "district": "์œ ์„ฑ๊ตฌ", + "address": "๋Œ€์ „์‹œ ์œ ์„ฑ๊ตฌ ๋Œ€ํ•™๋กœ 456" + }, + "businessHours": { + "weekday": "10:00-23:00", + "weekend": "10:00-23:00", + "holiday": "๋งค์ฃผ ์›”์š”์ผ ํœด๋ฌด" + }, + "staff": 2, + "seats": 30, + "monthlyRevenue": 18000000, + "avgCheckSize": 7000, + "menu": [ + {"name": "์•„๋ฉ”๋ฆฌ์นด๋…ธ", "price": 4000}, + {"name": "ํ—ˆ๋ธŒํ‹ฐ", "price": 5000}, + {"name": "์™€ํ”Œ", "price": 6000}, + {"name": "์ƒŒ๋“œ์œ„์น˜", "price": 7500} + ], + "targetCustomers": "๋Œ€ํ•™์ƒ, 20-30๋Œ€, ์Šคํ„ฐ๋”” ๊ทธ๋ฃน", + "strongPoints": "์กฐ์šฉํ•œ ๋ถ„์œ„๊ธฐ, ์žฅ์‹œ๊ฐ„ ์ด์šฉ ๊ฐ€๋Šฅ, ๋„์„œ ๋Œ€์—ฌ", + "locationCharacter": "๋Œ€ํ•™๊ฐ€ ์ƒ๊ถŒ, ์‹œํ—˜ ๊ธฐ๊ฐ„ ๋งค์ถœ ์ฆ๊ฐ€" +} +``` + +### 2-3. ๋”๋ธ”๋ž™์ปคํ”ผ +```json +{ + "storeId": "CF003", + "storeName": "๋”๋ธ”๋ž™์ปคํ”ผ", + "category": "cafe", + "categoryKo": "์นดํŽ˜", + "location": { + "city": "๊ฒฝ๊ธฐ", + "district": "์ˆ˜์›์‹œ", + "address": "๊ฒฝ๊ธฐ๋„ ์ˆ˜์›์‹œ ํŒ”๋‹ฌ๊ตฌ ํ–‰๊ถ๋กœ 789" + }, + "businessHours": { + "weekday": "08:00-20:00", + "weekend": "09:00-20:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 3, + "seats": 20, + "monthlyRevenue": 25000000, + "avgCheckSize": 6500, + "menu": [ + {"name": "์•„๋ฉ”๋ฆฌ์นด๋…ธ", "price": 3500}, + {"name": "์นดํŽ˜๋ผ๋–ผ", "price": 4000}, + {"name": "ํ•ธ๋“œ๋“œ๋ฆฝ ์ปคํ”ผ", "price": 6000}, + {"name": "๋””์ €ํŠธ ์„ธํŠธ", "price": 8000} + ], + "targetCustomers": "์ง์žฅ์ธ, 30-50๋Œ€, ํ…Œ์ดํฌ์•„์›ƒ ๊ณ ๊ฐ", + "strongPoints": "ํ•ฉ๋ฆฌ์  ๊ฐ€๊ฒฉ, ์ปคํ”ผ ํ’ˆ์งˆ, ํ…Œ์ดํฌ์•„์›ƒ ํ™œ์„ฑํ™”", + "locationCharacter": "์ˆ˜์› ํ–‰๊ถ๋™, ๊ด€๊ด‘๊ฐ ๋ฐ ์ง€์—ญ ์ฃผ๋ฏผ ํ˜ผ์žฌ" +} +``` + +--- + +## 3. Chinese Restaurant (์ค‘๊ตญ ์Œ์‹์ ) + +### 3-1. ์šฉ๊ถ์งœ์žฅ +```json +{ + "storeId": "CN001", + "storeName": "์šฉ๊ถ์งœ์žฅ", + "category": "chinese_restaurant", + "categoryKo": "์ค‘๊ตญ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "์˜๋“ฑํฌ๊ตฌ", + "address": "์„œ์šธ์‹œ ์˜๋“ฑํฌ๊ตฌ ์—ฌ์˜๋Œ€๋กœ 123" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "12:00-21:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 4, + "seats": 35, + "monthlyRevenue": 32000000, + "avgCheckSize": 10000, + "menu": [ + {"name": "์งœ์žฅ๋ฉด", "price": 6000}, + {"name": "์งฌ๋ฝ•", "price": 7000}, + {"name": "ํƒ•์ˆ˜์œก", "price": 18000}, + {"name": "๊นํ’๊ธฐ", "price": 20000} + ], + "targetCustomers": "์ง์žฅ์ธ, ๊ฐ€์กฑ, ๋ฐฐ๋‹ฌ ๊ณ ๊ฐ", + "strongPoints": "๋ฐฐ๋‹ฌ ๋น ๋ฆ„, ํ‘ธ์งํ•œ ์–‘, ์ ์‹ฌ ํŠน์„ ", + "locationCharacter": "์—ฌ์˜๋„ ์—…๋ฌด์ง€๊ตฌ, ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ ๋งŽ์Œ" +} +``` + +### 3-2. ํ™ฉ์ œ๋ฃจ +```json +{ + "storeId": "CN002", + "storeName": "ํ™ฉ์ œ๋ฃจ", + "category": "chinese_restaurant", + "categoryKo": "์ค‘๊ตญ ์Œ์‹์ ", + "location": { + "city": "์ธ์ฒœ", + "district": "์ค‘๊ตฌ", + "address": "์ธ์ฒœ์‹œ ์ค‘๊ตฌ ์ฐจ์ด๋‚˜ํƒ€์šด๋กœ 456" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 6, + "seats": 50, + "monthlyRevenue": 55000000, + "avgCheckSize": 22000, + "menu": [ + {"name": "์ฝ”์Šค A", "price": 35000}, + {"name": "์ฝ”์Šค B", "price": 50000}, + {"name": "๋ถ๊ฒฝ์˜ค๋ฆฌ", "price": 45000}, + {"name": "๋งˆ๋ผ์ƒน๊ถˆ", "price": 28000} + ], + "targetCustomers": "๊ด€๊ด‘๊ฐ, 30-50๋Œ€, ๋‹จ์ฒด ๋ชจ์ž„", + "strongPoints": "์ •ํ†ต ์ค‘์‹, ๋‹จ์ฒด์„ ์™„๋น„, ๊ด€๊ด‘์ง€ ์œ„์น˜", + "locationCharacter": "์ฐจ์ด๋‚˜ํƒ€์šด, ์ฃผ๋ง ๋งค์ถœ ๋†’์Œ" +} +``` + +### 3-3. ๋™๋„ค์ค‘๊ตญ์ง‘ +```json +{ + "storeId": "CN003", + "storeName": "๋™๋„ค์ค‘๊ตญ์ง‘", + "category": "chinese_restaurant", + "categoryKo": "์ค‘๊ตญ ์Œ์‹์ ", + "location": { + "city": "๋Œ€๊ตฌ", + "district": "์ˆ˜์„ฑ๊ตฌ", + "address": "๋Œ€๊ตฌ์‹œ ์ˆ˜์„ฑ๊ตฌ ๋ฒ”์–ด๋™ 789" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "๋งค์ฃผ ์›”์š”์ผ ํœด๋ฌด" + }, + "staff": 3, + "seats": 20, + "monthlyRevenue": 24000000, + "avgCheckSize": 9000, + "menu": [ + {"name": "์งœ์žฅ๋ฉด", "price": 5500}, + {"name": "์งฌ๋ฝ•", "price": 6500}, + {"name": "์šฐ๋™", "price": 6000}, + {"name": "๋ณถ์Œ๋ฐฅ", "price": 7000} + ], + "targetCustomers": "์ง€์—ญ ์ฃผ๋ฏผ, 1์ธ ๊ณ ๊ฐ, ๋ฐฐ๋‹ฌ ๊ณ ๊ฐ", + "strongPoints": "๋น ๋ฅธ ๋ฐฐ๋‹ฌ, ๊ฐ€์„ฑ๋น„, ๋‹จ๊ณจ ๊ณ ๊ฐ ๋งŽ์Œ", + "locationCharacter": "์ฃผํƒ๊ฐ€ ์ƒ๊ถŒ, ์ €๋… ๋ฐฐ๋‹ฌ ๋งŽ์Œ" +} +``` + +--- + +## 4. Japanese Restaurant (์ผ๋ณธ ์Œ์‹์ ) + +### 4-1. ์Šค์‹œ๋งˆ๋ฃจ +```json +{ + "storeId": "JP001", + "storeName": "์Šค์‹œ๋งˆ๋ฃจ", + "category": "japanese_restaurant", + "categoryKo": "์ผ๋ณธ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "๊ฐ•๋‚จ๊ตฌ", + "address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ๋…ผํ˜„๋กœ 234" + }, + "businessHours": { + "weekday": "12:00-22:00", + "weekend": "12:00-23:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 5, + "seats": 30, + "monthlyRevenue": 62000000, + "avgCheckSize": 35000, + "menu": [ + {"name": "์ดˆ๋ฐฅ ์„ธํŠธ A", "price": 28000}, + {"name": "์ดˆ๋ฐฅ ์„ธํŠธ B", "price": 45000}, + {"name": "ํšŒ๋ฎ๋ฐฅ", "price": 18000}, + {"name": "์šฐ๋™์ •์‹", "price": 15000} + ], + "targetCustomers": "30-40๋Œ€, ๊ณ ์†Œ๋“์ธต, ๋ฐ์ดํŠธ ์ปคํ”Œ", + "strongPoints": "์‹ ์„ ํ•œ ์žฌ๋ฃŒ, ์˜ค๋งˆ์นด์„ธ, ํ”„๋ฆฌ๋ฏธ์—„ ์ด๋ฏธ์ง€", + "locationCharacter": "๊ฐ•๋‚จ ํ•ต์‹ฌ ์ƒ๊ถŒ, ์ €๋… ๋งค์ถœ ๋†’์Œ" +} +``` + +### 4-2. ๋ผ๋ฉ˜์ง‘ +```json +{ + "storeId": "JP002", + "storeName": "๋ผ๋ฉ˜์ง‘", + "category": "ramen_restaurant", + "categoryKo": "๋ผ๋ฉ˜ ์ „๋ฌธ์ ", + "location": { + "city": "์„œ์šธ", + "district": "ํ™๋Œ€", + "address": "์„œ์šธ์‹œ ๋งˆํฌ๊ตฌ ํ™๋Œ€์ž…๊ตฌ์—ญ 456" + }, + "businessHours": { + "weekday": "11:30-22:00", + "weekend": "11:30-23:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 3, + "seats": 18, + "monthlyRevenue": 35000000, + "avgCheckSize": 12000, + "menu": [ + {"name": "๋ˆ์ฝ”์ธ ๋ผ๋ฉ˜", "price": 9500}, + {"name": "์‡ผ์œ ๋ผ๋ฉ˜", "price": 9000}, + {"name": "์ฐจ์Šˆ๋ฎ๋ฐฅ", "price": 8000}, + {"name": "๊ต์ž", "price": 6000} + ], + "targetCustomers": "20-30๋Œ€, ๋Œ€ํ•™์ƒ, ์ง์žฅ์ธ", + "strongPoints": "์ง„ํ•œ ๊ตญ๋ฌผ, ๋น ๋ฅธ ํšŒ์ „์œจ, 1์ธ ์‹์‚ฌ ์ตœ์ ํ™”", + "locationCharacter": "ํ™๋Œ€ ๋ฒˆํ™”๊ฐ€, ์ ์‹ฌยท์ €๋… ํ”ผํฌํƒ€์ž„ ์›จ์ดํŒ…" +} +``` + +### 4-3. ํ…๋™์•ผ +```json +{ + "storeId": "JP003", + "storeName": "ํ…๋™์•ผ", + "category": "japanese_restaurant", + "categoryKo": "์ผ๋ณธ ์Œ์‹์ ", + "location": { + "city": "๋ถ€์‚ฐ", + "district": "์„œ๋ฉด", + "address": "๋ถ€์‚ฐ์‹œ ๋ถ€์‚ฐ์ง„๊ตฌ ์„œ๋ฉด๋กœ 789" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "๋งค์ฃผ ์ผ์š”์ผ ํœด๋ฌด" + }, + "staff": 4, + "seats": 25, + "monthlyRevenue": 28000000, + "avgCheckSize": 11000, + "menu": [ + {"name": "ํ…๋™", "price": 9500}, + {"name": "ํŠนํ…๋™", "price": 12000}, + {"name": "์นด์ธ ๋™", "price": 10000}, + {"name": "์šฐ๋™์„ธํŠธ", "price": 13000} + ], + "targetCustomers": "20-40๋Œ€, ์ง์žฅ์ธ, ์ ์‹ฌ ๊ณ ๊ฐ", + "strongPoints": "๋น ๋ฅธ ์„œ๋น™, ํ•ฉ๋ฆฌ์  ๊ฐ€๊ฒฉ, ์ ์‹ฌ ํŠน์„ ", + "locationCharacter": "์„œ๋ฉด ์ƒ์—…์ง€๊ตฌ, ์ ์‹ฌ ๋งค์ถœ ์ง‘์ค‘" +} +``` + +--- + +## 5. Barbecue Restaurant (๋ฐ”๋ฒ ํ ์Œ์‹์ ) + +### 5-1. ํ•œ์šฐ๋งˆ์„ +```json +{ + "storeId": "BBQ001", + "storeName": "ํ•œ์šฐ๋งˆ์„", + "category": "barbecue_restaurant", + "categoryKo": "๋ฐ”๋ฒ ํ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "์†กํŒŒ๊ตฌ", + "address": "์„œ์šธ์‹œ ์†กํŒŒ๊ตฌ ์˜ฌ๋ฆผํ”ฝ๋กœ 123" + }, + "businessHours": { + "weekday": "17:00-24:00", + "weekend": "16:00-24:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 7, + "seats": 60, + "monthlyRevenue": 85000000, + "avgCheckSize": 45000, + "menu": [ + {"name": "ํ•œ์šฐ ๋“ฑ์‹ฌ (100g)", "price": 25000}, + {"name": "ํ•œ์šฐ ์•ˆ์‹ฌ (100g)", "price": 28000}, + {"name": "๊ฐˆ๋น„์‚ด (100g)", "price": 22000}, + {"name": "์œกํšŒ", "price": 35000} + ], + "targetCustomers": "30-50๋Œ€, ๊ฐ€์กฑ ๋ชจ์ž„, ํšŒ์‹", + "strongPoints": "1++๋“ฑ๊ธ‰ ํ•œ์šฐ, ๋„“์€ ๋ฃธ, ์ฃผ์ฐจ ํŽธ๋ฆฌ", + "locationCharacter": "์ž ์‹ค ์ƒ๊ถŒ, ์ €๋…ยท์ฃผ๋ง ์˜ˆ์•ฝ ํ•„์ˆ˜" +} +``` + +### 5-2. ๋ผ์ง€๋ž‘์†Œ๋ž‘ +```json +{ + "storeId": "BBQ002", + "storeName": "๋ผ์ง€๋ž‘์†Œ๋ž‘", + "category": "barbecue_restaurant", + "categoryKo": "๋ฐ”๋ฒ ํ ์Œ์‹์ ", + "location": { + "city": "๋Œ€์ „", + "district": "์„œ๊ตฌ", + "address": "๋Œ€์ „์‹œ ์„œ๊ตฌ ๋‘”์‚ฐ๋กœ 456" + }, + "businessHours": { + "weekday": "16:00-23:00", + "weekend": "15:00-23:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 4, + "seats": 35, + "monthlyRevenue": 42000000, + "avgCheckSize": 28000, + "menu": [ + {"name": "์‚ผ๊ฒน์‚ด (200g)", "price": 12000}, + {"name": "๋ชฉ์‚ด (200g)", "price": 13000}, + {"name": "ํ•ญ์ •์‚ด (200g)", "price": 15000}, + {"name": "์†Œ๊ณ ๊ธฐ (100g)", "price": 18000} + ], + "targetCustomers": "20-40๋Œ€, ํšŒ์‹, ๋ชจ์ž„", + "strongPoints": "๋ฌดํ•œ ๋ฆฌํ•„ ๋ฐ˜์ฐฌ, ์ง์ ‘ ๊ตฌ์›Œ์ฃผ๋Š” ์„œ๋น„์Šค, ์ €๋ ดํ•œ ๊ฐ€๊ฒฉ", + "locationCharacter": "๋‘”์‚ฐ๋™ ์ƒ๊ถŒ, ์ €๋… ๋งค์ถœ ์ง‘์ค‘" +} +``` + +### 5-3. ์ˆฏ๋ถˆ๊ฐˆ๋น„ +```json +{ + "storeId": "BBQ003", + "storeName": "์ˆฏ๋ถˆ๊ฐˆ๋น„", + "category": "barbecue_restaurant", + "categoryKo": "๋ฐ”๋ฒ ํ ์Œ์‹์ ", + "location": { + "city": "์ˆ˜์›", + "district": "์žฅ์•ˆ๊ตฌ", + "address": "๊ฒฝ๊ธฐ๋„ ์ˆ˜์›์‹œ ์žฅ์•ˆ๊ตฌ ์ •์กฐ๋กœ 789" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-22:00", + "holiday": "๋งค์ฃผ ์›”์š”์ผ ํœด๋ฌด" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 48000000, + "avgCheckSize": 32000, + "menu": [ + {"name": "LA๊ฐˆ๋น„", "price": 35000}, + {"name": "์–‘๋…๊ฐˆ๋น„", "price": 32000}, + {"name": "์†Œ๊ฐˆ๋น„์‚ด", "price": 38000}, + {"name": "๋ƒ‰๋ฉด", "price": 9000} + ], + "targetCustomers": "๊ฐ€์กฑ, 40-60๋Œ€, ์ง€์—ญ ์ฃผ๋ฏผ", + "strongPoints": "์ˆฏ๋ถˆ ์งํ™”, ์˜ค๋ž˜๋œ ๋‹จ๊ณจ ๊ณ ๊ฐ, ์ ์‹ฌ ๊ฐˆ๋น„ํƒ•", + "locationCharacter": "์ˆ˜์› ๊ตฌ๋„์‹ฌ, ์ ์‹ฌยท์ €๋… ๋ชจ๋‘ ํ™œ์„ฑํ™”" +} +``` + +--- + +## 6. Chicken Restaurant (์น˜ํ‚จ ์ „๋ฌธ์ ) + +### 6-1. ์ž๋‹ด์น˜ํ‚จ ์—ญ์‚ผ์  +```json +{ + "storeId": "CK001", + "storeName": "์ž๋‹ด์น˜ํ‚จ ์—ญ์‚ผ์ ", + "category": "chicken_restaurant", + "categoryKo": "์น˜ํ‚จ ์ „๋ฌธ์ ", + "location": { + "city": "์„œ์šธ", + "district": "๊ฐ•๋‚จ๊ตฌ", + "address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 123" + }, + "businessHours": { + "weekday": "15:00-03:00", + "weekend": "15:00-04:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 3, + "seats": 0, + "monthlyRevenue": 35000000, + "avgCheckSize": 24000, + "menu": [ + {"name": "ํ›„๋ผ์ด๋“œ", "price": 18000}, + {"name": "์–‘๋…์น˜ํ‚จ", "price": 19000}, + {"name": "๊ฐ„์žฅ์น˜ํ‚จ", "price": 19000}, + {"name": "์ˆœ์‚ด์น˜ํ‚จ", "price": 20000} + ], + "targetCustomers": "20-40๋Œ€, ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ ๊ณ ๊ฐ, ์•ผ๊ฐ„ ์ฃผ๋ฌธ", + "strongPoints": "๋ฐฐ๋‹ฌ ์ „๋ฌธ, ๋น ๋ฅธ ๋ฐฐ๋‹ฌ, ๋งˆ์ผ€ํŒ… ์ง€์›", + "locationCharacter": "์—ญ์‚ผ ์ƒ์—…์ง€๊ตฌ, ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ 95%" +} +``` + +### 6-2. ํฌ์น˜์น˜ํ‚จ +```json +{ + "storeId": "CK002", + "storeName": "ํฌ์น˜์น˜ํ‚จ", + "category": "chicken_restaurant", + "categoryKo": "์น˜ํ‚จ ์ „๋ฌธ์ ", + "location": { + "city": "์„œ์šธ", + "district": "๊ด€์•…๊ตฌ", + "address": "์„œ์šธ์‹œ ๊ด€์•…๊ตฌ ์‹ ๋ฆผ๋™ 456" + }, + "businessHours": { + "weekday": "16:00-02:00", + "weekend": "14:00-03:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 2, + "seats": 15, + "monthlyRevenue": 28000000, + "avgCheckSize": 22000, + "menu": [ + {"name": "์ˆœ์‚ด์น˜ํ‚จ 2๋งˆ๋ฆฌ", "price": 20000}, + {"name": "ํฌ์น˜๋ฐ์ด ํŠน๊ฐ€", "price": 10000}, + {"name": "์–‘๋…์น˜ํ‚จ", "price": 18000}, + {"name": "ํ›„๋ผ์ด๋“œ", "price": 17000} + ], + "targetCustomers": "๋Œ€ํ•™์ƒ, 20-30๋Œ€, ์ง€์—ญ ์ฃผ๋ฏผ", + "strongPoints": "๊ฐ์„ฑ ์ธํ…Œ๋ฆฌ์–ด, ์ €๋ ดํ•œ ๊ฐ€๊ฒฉ, ํฌ์น˜๋ฐ์ด ์ด๋ฒคํŠธ", + "locationCharacter": "๋Œ€ํ•™๊ฐ€ ์ƒ๊ถŒ, ์ €๋…ยท์•ผ๊ฐ„ ๋งค์ถœ ์ง‘์ค‘" +} +``` + +### 6-3. ๋™๋„ค์น˜ํ‚จ +```json +{ + "storeId": "CK003", + "storeName": "๋™๋„ค์น˜ํ‚จ", + "category": "chicken_restaurant", + "categoryKo": "์น˜ํ‚จ ์ „๋ฌธ์ ", + "location": { + "city": "์ธ์ฒœ", + "district": "๋‚จ๋™๊ตฌ", + "address": "์ธ์ฒœ์‹œ ๋‚จ๋™๊ตฌ ๊ตฌ์›”๋™ 789" + }, + "businessHours": { + "weekday": "15:00-01:00", + "weekend": "15:00-02:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 2, + "seats": 0, + "monthlyRevenue": 22000000, + "avgCheckSize": 20000, + "menu": [ + {"name": "ํ›„๋ผ์ด๋“œ", "price": 16000}, + {"name": "์–‘๋…์น˜ํ‚จ", "price": 17000}, + {"name": "๋ฐ˜๋ฐ˜์น˜ํ‚จ", "price": 17000}, + {"name": "์น˜ํ‚จ๋ฌด", "price": 0} + ], + "targetCustomers": "์ง€์—ญ ์ฃผ๋ฏผ, ๊ฐ€์กฑ, ๋ฐฐ๋‹ฌ ๊ณ ๊ฐ", + "strongPoints": "๋‹จ๊ณจ ๊ณ ๊ฐ, ์‹ ์† ๋ฐฐ๋‹ฌ, ํ”„๋ Œ์น˜ํ”„๋ผ์ด ์„œ๋น„์Šค", + "locationCharacter": "์ฃผํƒ๊ฐ€ ์ƒ๊ถŒ, ์ €๋… ์ฃผ๋ฌธ ๋งŽ์Œ" +} +``` + +--- + +## 7. Pizza Restaurant (ํ”ผ์ž ์ „๋ฌธ์ ) + +### 7-1. ํ”ผ์ž์Šค์ฟจ +```json +{ + "storeId": "PZ001", + "storeName": "ํ”ผ์ž์Šค์ฟจ", + "category": "pizza_restaurant", + "categoryKo": "ํ”ผ์ž ์ „๋ฌธ์ ", + "location": { + "city": "์„œ์šธ", + "district": "๋…ธ์›๊ตฌ", + "address": "์„œ์šธ์‹œ ๋…ธ์›๊ตฌ ์ƒ๊ณ„๋™ 123" + }, + "businessHours": { + "weekday": "11:00-23:00", + "weekend": "11:00-23:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 3, + "seats": 10, + "monthlyRevenue": 26000000, + "avgCheckSize": 15000, + "menu": [ + {"name": "์ฝค๋น„๋„ค์ด์…˜ ํ”ผ์ž", "price": 12000}, + {"name": "๋ถˆ๊ณ ๊ธฐ ํ”ผ์ž", "price": 13000}, + {"name": "ํฌํ…Œ์ดํ†  ํ”ผ์ž", "price": 11000}, + {"name": "์น˜์ฆˆ ํฌ๋Ÿฌ์ŠคํŠธ", "price": 2000} + ], + "targetCustomers": "๊ฐ€์กฑ, ํ•™์ƒ, ๋ฐฐ๋‹ฌ ๊ณ ๊ฐ", + "strongPoints": "ํ•ฉ๋ฆฌ์  ๊ฐ€๊ฒฉ, 1+1 ์ด๋ฒคํŠธ, ๋น ๋ฅธ ๋ฐฐ๋‹ฌ", + "locationCharacter": "์ฃผํƒ๊ฐ€ ์ƒ๊ถŒ, ์ฃผ๋ง ์ฃผ๋ฌธ ๋งŽ์Œ" +} +``` + +### 7-2. ์ดํƒœ๋ฆฌ์•„๋…ธ +```json +{ + "storeId": "PZ002", + "storeName": "์ดํƒœ๋ฆฌ์•„๋…ธ", + "category": "pizza_restaurant", + "categoryKo": "ํ”ผ์ž ์ „๋ฌธ์ ", + "location": { + "city": "์„œ์šธ", + "district": "์ดํƒœ์›", + "address": "์„œ์šธ์‹œ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 456" + }, + "businessHours": { + "weekday": "12:00-23:00", + "weekend": "12:00-24:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 52000000, + "avgCheckSize": 32000, + "menu": [ + {"name": "๋งˆ๋ฅด๊ฒŒ๋ฆฌํƒ€", "price": 22000}, + {"name": "๋””์•„๋ณผ๋ผ", "price": 25000}, + {"name": "์ฝฐํŠธ๋กœ ํฌ๋ฅด๋งˆ์ง€", "price": 28000}, + {"name": "ํŒŒ์Šคํƒ€", "price": 18000} + ], + "targetCustomers": "20-40๋Œ€, ๋ฐ์ดํŠธ ์ปคํ”Œ, ์™ธ๊ตญ์ธ", + "strongPoints": "ํ™”๋• ํ”ผ์ž, ์ •ํ†ต ์ดํƒˆ๋ฆฌ์•ˆ, ์™€์ธ ํŽ˜์–ด๋ง", + "locationCharacter": "์ดํƒœ์› ํ•ซํ”Œ๋ ˆ์ด์Šค, ์ €๋…ยท์ฃผ๋ง ๋งค์ถœ ๋†’์Œ" +} +``` + +### 7-3. ํŒŒํŒŒ์กด์Šค +```json +{ + "storeId": "PZ003", + "storeName": "ํŒŒํŒŒ์กด์Šค", + "category": "pizza_restaurant", + "categoryKo": "ํ”ผ์ž ์ „๋ฌธ์ ", + "location": { + "city": "๋ถ€์‚ฐ", + "district": "ํ•ด์šด๋Œ€๊ตฌ", + "address": "๋ถ€์‚ฐ์‹œ ํ•ด์šด๋Œ€๊ตฌ ์„ผํ…€์‹œํ‹ฐ 789" + }, + "businessHours": { + "weekday": "11:00-23:00", + "weekend": "11:00-24:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 4, + "seats": 20, + "monthlyRevenue": 38000000, + "avgCheckSize": 28000, + "menu": [ + {"name": "์ŠˆํผํŒŒํŒŒ์Šค", "price": 25000}, + {"name": "ํ•˜์™€์ด์•ˆ", "price": 23000}, + {"name": "ํŽ˜ํผ๋กœ๋‹ˆ", "price": 24000}, + {"name": "์น˜์ฆˆ์Šคํ‹ฑ", "price": 8000} + ], + "targetCustomers": "๊ฐ€์กฑ, 20-40๋Œ€, ํŒŒํ‹ฐ ์ฃผ๋ฌธ", + "strongPoints": "ํ”„๋ฆฌ๋ฏธ์—„ ๋ธŒ๋žœ๋“œ, ๋‹ค์–‘ํ•œ ์‚ฌ์ด๋“œ ๋ฉ”๋‰ด, ์•ฑ ์ฃผ๋ฌธ ํŽธ๋ฆฌ", + "locationCharacter": "์„ผํ…€์‹œํ‹ฐ ์ƒ๊ถŒ, ์ฃผ๋ง ๋งค์ถœ ๋†’์Œ" +} +``` + +--- + +## 8. Bakery (์ œ๊ณผ์ ) + +### 8-1. ์•„๋œจ๋ฒ  ๋ฒ ์ด์ปค๋ฆฌ +```json +{ + "storeId": "BK001", + "storeName": "์•„๋œจ๋ฒ  ๋ฒ ์ด์ปค๋ฆฌ", + "category": "bakery", + "categoryKo": "์ œ๊ณผ์ ", + "location": { + "city": "์„œ์šธ", + "district": "์„ฑ๋™๊ตฌ", + "address": "์„œ์šธ์‹œ ์„ฑ๋™๊ตฌ ์™•์‹ญ๋ฆฌ๋กœ 123" + }, + "businessHours": { + "weekday": "00:00-24:00", + "weekend": "00:00-24:00", + "holiday": "24์‹œ๊ฐ„ ์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 6, + "seats": 25, + "monthlyRevenue": 65000000, + "avgCheckSize": 12000, + "menu": [ + {"name": "ํฌ๋ฃจ์•„์ƒ", "price": 3500}, + {"name": "์†Œ๊ธˆ๋นต", "price": 2500}, + {"name": "๋ฒ ์ด๊ธ€", "price": 3000}, + {"name": "์ผ€์ดํฌ", "price": 35000} + ], + "targetCustomers": "์ „ ์—ฐ๋ น๋Œ€, ์•ผ๊ฐ„ ๊ณ ๊ฐ, ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ", + "strongPoints": "24์‹œ๊ฐ„ ์šด์˜, ์‹ ์„ ํ•œ ๋นต, ๋‹ค์–‘ํ•œ ๋ฉ”๋‰ด", + "locationCharacter": "์™•์‹ญ๋ฆฌ ์—ญ์„ธ๊ถŒ, ์•ผ๊ฐ„ ๋งค์ถœ 30%" +} +``` + +### 8-2. ๋™๋„ค๋นต์ง‘ +```json +{ + "storeId": "BK002", + "storeName": "๋™๋„ค๋นต์ง‘", + "category": "bakery", + "categoryKo": "์ œ๊ณผ์ ", + "location": { + "city": "๋Œ€๊ตฌ", + "district": "์ค‘๊ตฌ", + "address": "๋Œ€๊ตฌ์‹œ ์ค‘๊ตฌ ๋™์„ฑ๋กœ 456" + }, + "businessHours": { + "weekday": "07:00-22:00", + "weekend": "08:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 3, + "seats": 0, + "monthlyRevenue": 24000000, + "avgCheckSize": 8000, + "menu": [ + {"name": "์‹๋นต", "price": 4000}, + {"name": "๋‹จํŒฅ๋นต", "price": 2000}, + {"name": "ํฌ๋ฆผ๋นต", "price": 2500}, + {"name": "์ƒ์ผ ์ผ€์ดํฌ", "price": 25000} + ], + "targetCustomers": "์ง€์—ญ ์ฃผ๋ฏผ, 40-60๋Œ€, ์•„์นจ ๊ณ ๊ฐ", + "strongPoints": "์˜ค๋žœ ์—ญ์‚ฌ, ๋‹จ๊ณจ ๊ณ ๊ฐ, ํ•ฉ๋ฆฌ์  ๊ฐ€๊ฒฉ", + "locationCharacter": "๋™์„ฑ๋กœ ์ƒ๊ถŒ, ์•„์นจ ๋งค์ถœ ๋†’์Œ" +} +``` + +### 8-3. ํŒŒ๋ฆฌ๋ฐ”๊ฒŒํŠธ +```json +{ + "storeId": "BK003", + "storeName": "ํŒŒ๋ฆฌ๋ฐ”๊ฒŒํŠธ", + "category": "bakery", + "categoryKo": "์ œ๊ณผ์ ", + "location": { + "city": "์ธ์ฒœ", + "district": "์—ฐ์ˆ˜๊ตฌ", + "address": "์ธ์ฒœ์‹œ ์—ฐ์ˆ˜๊ตฌ ์†ก๋„๋™ 789" + }, + "businessHours": { + "weekday": "07:00-23:00", + "weekend": "07:00-23:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 5, + "seats": 15, + "monthlyRevenue": 48000000, + "avgCheckSize": 10000, + "menu": [ + {"name": "ํฌ๋ฃจ์•„์ƒ", "price": 3000}, + {"name": "์†Œ๋ณด๋กœ", "price": 2500}, + {"name": "์ƒŒ๋“œ์œ„์น˜", "price": 4500}, + {"name": "์ผ€์ดํฌ", "price": 30000} + ], + "targetCustomers": "์ง์žฅ์ธ, ํ•™์ƒ, ๊ฐ€์กฑ", + "strongPoints": "๋ธŒ๋žœ๋“œ ์ธ์ง€๋„, ๋‹ค์–‘ํ•œ ์ œํ’ˆ, ๋ฉค๋ฒ„์‹ญ ํ˜œํƒ", + "locationCharacter": "์†ก๋„ ์‹ ๋„์‹œ, ์•„์นจยท์ €๋… ๋งค์ถœ ์ง‘์ค‘" +} +``` + +--- + +## 9. Coffee Shop (์ปคํ”ผ์ˆ) + +### 9-1. ๋ฉ”๊ฐ€์ปคํ”ผ +```json +{ + "storeId": "CS001", + "storeName": "๋ฉ”๊ฐ€์ปคํ”ผ", + "category": "coffee_shop", + "categoryKo": "์ปคํ”ผ์ˆ", + "location": { + "city": "์„œ์šธ", + "district": "๊ฐ•๋™๊ตฌ", + "address": "์„œ์šธ์‹œ ๊ฐ•๋™๊ตฌ ์ฒœํ˜ธ๋Œ€๋กœ 123" + }, + "businessHours": { + "weekday": "07:00-22:00", + "weekend": "08:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 2, + "seats": 10, + "monthlyRevenue": 18000000, + "avgCheckSize": 4500, + "menu": [ + {"name": "์•„๋ฉ”๋ฆฌ์นด๋…ธ", "price": 1500}, + {"name": "์นดํŽ˜๋ผ๋–ผ", "price": 2000}, + {"name": "๋ฐ”๋‹๋ผ๋ผ๋–ผ", "price": 2500}, + {"name": "์—์ด๋“œ", "price": 3000} + ], + "targetCustomers": "ํ•™์ƒ, 20-30๋Œ€, ํ…Œ์ดํฌ์•„์›ƒ ๊ณ ๊ฐ", + "strongPoints": "์ €๋ ดํ•œ ๊ฐ€๊ฒฉ, ๋น ๋ฅธ ์„œ๋น„์Šค, ์•ฑ ์ฃผ๋ฌธ", + "locationCharacter": "์ฒœํ˜ธ์—ญ ์—ญ์„ธ๊ถŒ, ์ถœํ‡ด๊ทผ ์‹œ๊ฐ„ ๋งค์ถœ ๋†’์Œ" +} +``` + +### 9-2. ์ปดํฌ์ฆˆ์ปคํ”ผ +```json +{ + "storeId": "CS002", + "storeName": "์ปดํฌ์ฆˆ์ปคํ”ผ", + "category": "coffee_shop", + "categoryKo": "์ปคํ”ผ์ˆ", + "location": { + "city": "๋ถ€์‚ฐ", + "district": "์‚ฌ์ƒ๊ตฌ", + "address": "๋ถ€์‚ฐ์‹œ ์‚ฌ์ƒ๊ตฌ ์ฃผ๋ก€๋™ 456" + }, + "businessHours": { + "weekday": "07:30-22:00", + "weekend": "08:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 2, + "seats": 8, + "monthlyRevenue": 16000000, + "avgCheckSize": 4000, + "menu": [ + {"name": "์•„๋ฉ”๋ฆฌ์นด๋…ธ", "price": 1500}, + {"name": "์นดํŽ˜๋ผ๋–ผ", "price": 2000}, + {"name": "๋”ธ๊ธฐ๋ผ๋–ผ", "price": 3500}, + {"name": "ํ•œ์ •ํŒ ์Œ๋ฃŒ", "price": 4000} + ], + "targetCustomers": "20-30๋Œ€, ํ•™์ƒ, ์ง์žฅ์ธ", + "strongPoints": "๊ฐ€์„ฑ๋น„, ์บ๋ฆญํ„ฐ ํ˜‘์—…, ๋ชจ๋ฐ”์ผ ์ฃผ๋ฌธ", + "locationCharacter": "์ฃผ๋ก€๋™ ์ƒ๊ถŒ, ํ…Œ์ดํฌ์•„์›ƒ 80%" +} +``` + +### 9-3. ์Šคํƒ€๋ฒ…์Šค +```json +{ + "storeId": "CS003", + "storeName": "์Šคํƒ€๋ฒ…์Šค", + "category": "coffee_shop", + "categoryKo": "์ปคํ”ผ์ˆ", + "location": { + "city": "์„œ์šธ", + "district": "๊ฐ•๋‚จ๊ตฌ", + "address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 789" + }, + "businessHours": { + "weekday": "07:00-22:00", + "weekend": "08:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 8, + "seats": 60, + "monthlyRevenue": 85000000, + "avgCheckSize": 8500, + "menu": [ + {"name": "์•„๋ฉ”๋ฆฌ์นด๋…ธ", "price": 4500}, + {"name": "์นดํŽ˜๋ผ๋–ผ", "price": 5500}, + {"name": "ํ”„๋ผํ‘ธ์น˜๋…ธ", "price": 6500}, + {"name": "์ผ€์ดํฌ", "price": 7000} + ], + "targetCustomers": "์ง์žฅ์ธ, 30-40๋Œ€, ํšŒ์˜ ๊ณ ๊ฐ", + "strongPoints": "๋ธŒ๋žœ๋“œ ํŒŒ์›Œ, ๋„“์€ ๊ณต๊ฐ„, ํ”„๋ฆฌ๋ฏธ์—„ ์ด๋ฏธ์ง€", + "locationCharacter": "๊ฐ•๋‚จ ์—…๋ฌด์ง€๊ตฌ, ์˜ค์ „ยท์ ์‹ฌ ๋งค์ถœ ๋†’์Œ" +} +``` + +--- + +## 10. American Restaurant (๋ฏธ๊ตญ ์Œ์‹์ ) + +### 10-1. ๋ฒ„๊ฑฐํ‚น +```json +{ + "storeId": "AM001", + "storeName": "๋ฒ„๊ฑฐํ‚น", + "category": "american_restaurant", + "categoryKo": "๋ฏธ๊ตญ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "์ข…๋กœ๊ตฌ", + "address": "์„œ์šธ์‹œ ์ข…๋กœ๊ตฌ ์ข…๊ฐ์—ญ 123" + }, + "businessHours": { + "weekday": "09:00-23:00", + "weekend": "09:00-23:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 6, + "seats": 50, + "monthlyRevenue": 62000000, + "avgCheckSize": 9500, + "menu": [ + {"name": "์™€ํผ", "price": 6900}, + {"name": "์น˜์ฆˆ์™€ํผ", "price": 7500}, + {"name": "๋กฑ์น˜ํ‚จ๋ฒ„๊ฑฐ", "price": 6500}, + {"name": "์„ธํŠธ ๋ฉ”๋‰ด", "price": 9900} + ], + "targetCustomers": "10-30๋Œ€, ํ•™์ƒ, ์ง์žฅ์ธ", + "strongPoints": "๋ธŒ๋žœ๋“œ ์ธ์ง€๋„, ๋น ๋ฅธ ์„œ๋น„์Šค, ์•ฑ ํ• ์ธ", + "locationCharacter": "์ข…๊ฐ์—ญ ์—ญ์„ธ๊ถŒ, ์ ์‹ฌ ์‹œ๊ฐ„ ๋ถ๋น”" +} +``` + +### 10-2. ์‰์ดํฌ์‰‘ +```json +{ + "storeId": "AM002", + "storeName": "์‰์ดํฌ์‰‘", + "category": "hamburger_restaurant", + "categoryKo": "ํ–„๋ฒ„๊ฑฐ ์ „๋ฌธ์ ", + "location": { + "city": "์„œ์šธ", + "district": "๊ฐ•๋‚จ๊ตฌ", + "address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 456" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 10, + "seats": 70, + "monthlyRevenue": 125000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "์‰‘๋ฒ„๊ฑฐ", "price": 9900}, + {"name": "์Šค๋ชจํฌ์‰‘", "price": 11900}, + {"name": "์น˜ํ‚จ๋ฒ„๊ฑฐ", "price": 10900}, + {"name": "์‰์ดํฌ", "price": 6900} + ], + "targetCustomers": "20-40๋Œ€, ํ”„๋ฆฌ๋ฏธ์—„ ์„ ํ˜ธ ๊ณ ๊ฐ", + "strongPoints": "ํ”„๋ฆฌ๋ฏธ์—„ ์žฌ๋ฃŒ, ๋งค์žฅ ๋ถ„์œ„๊ธฐ, ๋ธŒ๋žœ๋“œ ์ด๋ฏธ์ง€", + "locationCharacter": "๊ฐ•๋‚จ ํ•ต์‹ฌ ์ƒ๊ถŒ, ์ฃผ๋ง ์›จ์ดํŒ… ๋ฐœ์ƒ" +} +``` + +### 10-3. ๋งฅ๋„๋‚ ๋“œ +```json +{ + "storeId": "AM003", + "storeName": "๋งฅ๋„๋‚ ๋“œ", + "category": "fast_food_restaurant", + "categoryKo": "ํŒจ์ŠคํŠธํ‘ธ๋“œ์ ", + "location": { + "city": "์ธ์ฒœ", + "district": "๋ถ€ํ‰๊ตฌ", + "address": "์ธ์ฒœ์‹œ ๋ถ€ํ‰๊ตฌ ๋ถ€ํ‰๋Œ€๋กœ 789" + }, + "businessHours": { + "weekday": "00:00-24:00", + "weekend": "00:00-24:00", + "holiday": "24์‹œ๊ฐ„ ์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 15, + "seats": 80, + "monthlyRevenue": 142000000, + "avgCheckSize": 10500, + "menu": [ + {"name": "๋น…๋งฅ", "price": 6400}, + {"name": "1955 ๋ฒ„๊ฑฐ", "price": 8900}, + {"name": "์น˜์ฆˆ๋ฒ„๊ฑฐ", "price": 4500}, + {"name": "ํ•ดํ”ผ๋ฐ€", "price": 6500} + ], + "targetCustomers": "์ „ ์—ฐ๋ น๋Œ€, ๊ฐ€์กฑ, ํ•™์ƒ", + "strongPoints": "24์‹œ๊ฐ„ ์šด์˜, ๋“œ๋ผ์ด๋ธŒ์Šค๋ฃจ, ๋งฅ๋”œ๋ฆฌ๋ฒ„๋ฆฌ", + "locationCharacter": "๋ถ€ํ‰์—ญ ์—ญ์„ธ๊ถŒ, 24์‹œ๊ฐ„ ๊ณ ๋ฅธ ๋งค์ถœ" +} +``` + +--- + +## 11. Italian Restaurant (์ดํƒˆ๋ฆฌ์•„ ์Œ์‹์ ) + +### 11-1. ๋ฒ ๋„ค์น˜์•„ +```json +{ + "storeId": "IT001", + "storeName": "๋ฒ ๋„ค์น˜์•„", + "category": "italian_restaurant", + "categoryKo": "์ดํƒˆ๋ฆฌ์•„ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "์šฉ์‚ฐ๊ตฌ", + "address": "์„œ์šธ์‹œ ์šฉ์‚ฐ๊ตฌ ํ•œ๋‚จ๋™ 123" + }, + "businessHours": { + "weekday": "11:30-22:00", + "weekend": "11:30-22:00", + "holiday": "๋งค์ฃผ ์›”์š”์ผ ํœด๋ฌด" + }, + "staff": 7, + "seats": 45, + "monthlyRevenue": 68000000, + "avgCheckSize": 42000, + "menu": [ + {"name": "๊นŒ๋ฅด๋ณด๋‚˜๋ผ", "price": 22000}, + {"name": "์•Œ๋ฆฌ์˜ค์˜ฌ๋ฆฌ์˜ค", "price": 20000}, + {"name": "๋ฆฌ์กฐ๋˜", "price": 25000}, + {"name": "์Šคํ…Œ์ดํฌ", "price": 45000} + ], + "targetCustomers": "30-50๋Œ€, ๋ฐ์ดํŠธ, ๊ธฐ๋…์ผ", + "strongPoints": "์ •ํ†ต ์ดํƒˆ๋ฆฌ์•ˆ, ์™€์ธ ๋ฆฌ์ŠคํŠธ, ๋‚ญ๋งŒ์  ๋ถ„์œ„๊ธฐ", + "locationCharacter": "ํ•œ๋‚จ๋™ ๊ณ ๊ธ‰ ์ƒ๊ถŒ, ์ €๋… ์˜ˆ์•ฝ ํ•„์ˆ˜" +} +``` + +### 11-2. ํŒŒ์Šคํƒ€๊ณต์žฅ +```json +{ + "storeId": "IT002", + "storeName": "ํŒŒ์Šคํƒ€๊ณต์žฅ", + "category": "italian_restaurant", + "categoryKo": "์ดํƒˆ๋ฆฌ์•„ ์Œ์‹์ ", + "location": { + "city": "๋Œ€์ „", + "district": "์œ ์„ฑ๊ตฌ", + "address": "๋Œ€์ „์‹œ ์œ ์„ฑ๊ตฌ ๋ด‰๋ช…๋™ 456" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 32000000, + "avgCheckSize": 15000, + "menu": [ + {"name": "ํ† ๋งˆํ†  ํŒŒ์Šคํƒ€", "price": 12000}, + {"name": "ํฌ๋ฆผ ํŒŒ์Šคํƒ€", "price": 13000}, + {"name": "๋กœ์ œ ํŒŒ์Šคํƒ€", "price": 14000}, + {"name": "ํ•„๋ผํ”„", "price": 11000} + ], + "targetCustomers": "20-30๋Œ€, ๋Œ€ํ•™์ƒ, ๋ฐ์ดํŠธ", + "strongPoints": "๊ฐ€์„ฑ๋น„, ํ‘ธ์งํ•œ ์–‘, ๋Œ€ํ•™๊ฐ€ ์œ„์น˜", + "locationCharacter": "๋Œ€ํ•™๊ฐ€ ์ƒ๊ถŒ, ์ ์‹ฌยท์ €๋… ๋งค์ถœ ๊ท ๋“ฑ" +} +``` + +### 11-3. ํŠธ๋ผํ† ๋ฆฌ์•„ +```json +{ + "storeId": "IT003", + "storeName": "ํŠธ๋ผํ† ๋ฆฌ์•„", + "category": "italian_restaurant", + "categoryKo": "์ดํƒˆ๋ฆฌ์•„ ์Œ์‹์ ", + "location": { + "city": "๋ถ€์‚ฐ", + "district": "ํ•ด์šด๋Œ€๊ตฌ", + "address": "๋ถ€์‚ฐ์‹œ ํ•ด์šด๋Œ€๊ตฌ ๋งˆ๋ฆฐ์‹œํ‹ฐ 789" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 6, + "seats": 50, + "monthlyRevenue": 58000000, + "avgCheckSize": 38000, + "menu": [ + {"name": "ํ•ด์‚ฐ๋ฌผ ํŒŒ์Šคํƒ€", "price": 28000}, + {"name": "ํ”ผ์ž ๋งˆ๋ฅด๊ฒŒ๋ฆฌํƒ€", "price": 24000}, + {"name": "์•Œ๋ฆฌ์˜ค์˜ฌ๋ฆฌ์˜ค", "price": 22000}, + {"name": "์™€์ธ", "price": 45000} + ], + "targetCustomers": "30-50๋Œ€, ๊ฐ€์กฑ, ๊ด€๊ด‘๊ฐ", + "strongPoints": "์˜ค์…˜๋ทฐ, ์‹ ์„ ํ•œ ํ•ด์‚ฐ๋ฌผ, ๋ถ„์œ„๊ธฐ", + "locationCharacter": "๋งˆ๋ฆฐ์‹œํ‹ฐ ์ƒ๊ถŒ, ์ €๋…ยท์ฃผ๋ง ๋งค์ถœ ๋†’์Œ" +} +``` + +--- + +## 12. Thai Restaurant (ํƒœ๊ตญ ์Œ์‹์ ) + +### 12-1. ์จ๋•€ํ•˜์šฐ์Šค +```json +{ + "storeId": "TH001", + "storeName": "์จ๋•€ํ•˜์šฐ์Šค", + "category": "thai_restaurant", + "categoryKo": "ํƒœ๊ตญ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "์ดํƒœ์›", + "address": "์„œ์šธ์‹œ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 123" + }, + "businessHours": { + "weekday": "11:30-22:00", + "weekend": "11:30-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 5, + "seats": 35, + "monthlyRevenue": 42000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "ํŒŸํƒ€์ด", "price": 12000}, + {"name": "์จ๋•€", "price": 10000}, + {"name": "๋˜ ์–Œ๊ฟ", "price": 14000}, + {"name": "๊ทธ๋ฆฐ์ปค๋ฆฌ", "price": 15000} + ], + "targetCustomers": "20-40๋Œ€, ์™ธ๊ตญ์ธ, ๋ง›์ง‘ ํƒ๋ฐฉ๊ฐ", + "strongPoints": "์ •ํ†ต ํƒœ๊ตญ ์š”๋ฆฌ, ํƒœ๊ตญ์ธ ์…ฐํ”„, ๋งค์šด ๋ง› ๋‹จ๊ณ„ ์กฐ์ ˆ", + "locationCharacter": "์ดํƒœ์› ๋‹ค๋ฌธํ™” ์ƒ๊ถŒ, ์ €๋… ๋งค์ถœ ๋†’์Œ" +} +``` + +### 12-2. ๋ฐฉ์ฝ•ํ‚ค์นœ +```json +{ + "storeId": "TH002", + "storeName": "๋ฐฉ์ฝ•ํ‚ค์นœ", + "category": "thai_restaurant", + "categoryKo": "ํƒœ๊ตญ ์Œ์‹์ ", + "location": { + "city": "๊ฒฝ๊ธฐ", + "district": "์„ฑ๋‚จ์‹œ", + "address": "๊ฒฝ๊ธฐ๋„ ์„ฑ๋‚จ์‹œ ๋ถ„๋‹น๊ตฌ ์•ผํƒ‘๋™ 456" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "๋งค์ฃผ ์ผ์š”์ผ ํœด๋ฌด" + }, + "staff": 3, + "seats": 25, + "monthlyRevenue": 26000000, + "avgCheckSize": 14000, + "menu": [ + {"name": "ํŒŸํƒ€์ด", "price": 11000}, + {"name": "์นด์˜คํŒŸ", "price": 10000}, + {"name": "๋˜ ์–‘๊ฟ", "price": 13000}, + {"name": "๋ง๊ณ  ์Šคํ‹ฐํ‚ค๋ผ์ด์Šค", "price": 8000} + ], + "targetCustomers": "20-40๋Œ€, ๊ฐ€์กฑ, ์ง€์—ญ ์ฃผ๋ฏผ", + "strongPoints": "ํ•ฉ๋ฆฌ์  ๊ฐ€๊ฒฉ, ํ•œ๊ตญ์ธ ์ž…๋ง› ๊ณ ๋ ค, ์ ์‹ฌ ํŠน์„ ", + "locationCharacter": "๋ถ„๋‹น ์ฃผํƒ๊ฐ€, ์ ์‹ฌ ๋งค์ถœ ์ง‘์ค‘" +} +``` + +### 12-3. ํƒ€์ดํƒ€๋‹‰ +```json +{ + "storeId": "TH003", + "storeName": "ํƒ€์ดํƒ€๋‹‰", + "category": "thai_restaurant", + "categoryKo": "ํƒœ๊ตญ ์Œ์‹์ ", + "location": { + "city": "๋Œ€๊ตฌ", + "district": "์ค‘๊ตฌ", + "address": "๋Œ€๊ตฌ์‹œ ์ค‘๊ตฌ ๋™์„ฑ๋กœ 789" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 32000000, + "avgCheckSize": 16000, + "menu": [ + {"name": "ํŒŸํƒ€์ด", "price": 12000}, + {"name": "๋ฟŒํŒŸํ์ปค๋ฆฌ", "price": 18000}, + {"name": "๋˜ ์–Œ๊ฟ", "price": 14000}, + {"name": "์ฝ”์ฝ”๋„› ์•„์ด์Šคํฌ๋ฆผ", "price": 6000} + ], + "targetCustomers": "20-30๋Œ€, ๋ฐ์ดํŠธ, SNS ๊ณ ๊ฐ", + "strongPoints": "์ธ์Šคํƒ€๊ทธ๋ž˜๋จธ๋ธ” ๋ฉ”๋‰ด, ๋ถ„์œ„๊ธฐ, ๋‹ค์–‘ํ•œ ๋ฉ”๋‰ด", + "locationCharacter": "๋™์„ฑ๋กœ ๋ฒˆํ™”๊ฐ€, ์ €๋… ๋งค์ถœ ๋†’์Œ" +} +``` + +--- + +## 13. Vietnamese Restaurant (๋ฒ ํŠธ๋‚จ ์Œ์‹์ ) + +### 13-1. ํฌํ•˜๋…ธ์ด +```json +{ + "storeId": "VN001", + "storeName": "ํฌํ•˜๋…ธ์ด", + "category": "vietnamese_restaurant", + "categoryKo": "๋ฒ ํŠธ๋‚จ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "์˜๋“ฑํฌ๊ตฌ", + "address": "์„œ์šธ์‹œ ์˜๋“ฑํฌ๊ตฌ ๋Œ€๋ฆผ๋™ 123" + }, + "businessHours": { + "weekday": "10:00-22:00", + "weekend": "10:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 28000000, + "avgCheckSize": 11000, + "menu": [ + {"name": "์Œ€๊ตญ์ˆ˜", "price": 9000}, + {"name": "๋ถ„์งœ", "price": 10000}, + {"name": "๋ฐ˜๋ฏธ", "price": 7000}, + {"name": "์›”๋‚จ์Œˆ", "price": 12000} + ], + "targetCustomers": "๋ฒ ํŠธ๋‚จ์ธ, 20-40๋Œ€, ๋‹ค๋ฌธํ™” ๊ณ ๊ฐ", + "strongPoints": "์ •ํ†ต ๋ฒ ํŠธ๋‚จ ๋ง›, ๋ฒ ํŠธ๋‚จ์ธ ์…ฐํ”„, ์ €๋ ดํ•œ ๊ฐ€๊ฒฉ", + "locationCharacter": "๋Œ€๋ฆผ๋™ ์ฐจ์ด๋‚˜ํƒ€์šด, ๋‹ค๋ฌธํ™” ์ƒ๊ถŒ" +} +``` + +### 13-2. ๋ฏธ์Šค์‚ฌ์ด๊ณต +```json +{ + "storeId": "VN002", + "storeName": "๋ฏธ์Šค์‚ฌ์ด๊ณต", + "category": "vietnamese_restaurant", + "categoryKo": "๋ฒ ํŠธ๋‚จ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "๊ฐ•๋‚จ๊ตฌ", + "address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™ 456" + }, + "businessHours": { + "weekday": "11:00-21:30", + "weekend": "11:00-21:30", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 38000000, + "avgCheckSize": 14000, + "menu": [ + {"name": "์Œ€๊ตญ์ˆ˜", "price": 11000}, + {"name": "๋ถ„์งœ", "price": 13000}, + {"name": "๋ฐ˜๋ฏธ", "price": 8000}, + {"name": "๋ฒ ํŠธ๋‚จ ์ปคํ”ผ", "price": 5000} + ], + "targetCustomers": "์ง์žฅ์ธ, 20-40๋Œ€, ์ ์‹ฌ ๊ณ ๊ฐ", + "strongPoints": "๊น”๋”ํ•œ ์ธํ…Œ๋ฆฌ์–ด, ๋น ๋ฅธ ์„œ๋น„์Šค, ์ง์žฅ์ธ ํŠนํ™”", + "locationCharacter": "์—ญ์‚ผ ์—…๋ฌด์ง€๊ตฌ, ์ ์‹ฌ ๋งค์ถœ 70%" +} +``` + +### 13-3. ํฌํƒ€์šด +```json +{ + "storeId": "VN003", + "storeName": "ํฌํƒ€์šด", + "category": "vietnamese_restaurant", + "categoryKo": "๋ฒ ํŠธ๋‚จ ์Œ์‹์ ", + "location": { + "city": "๋ถ€์‚ฐ", + "district": "์ง„๊ตฌ", + "address": "๋ถ€์‚ฐ์‹œ ๋ถ€์‚ฐ์ง„๊ตฌ ์„œ๋ฉด๋กœ 789" + }, + "businessHours": { + "weekday": "10:00-21:00", + "weekend": "10:00-21:00", + "holiday": "๋งค์ฃผ ์ผ์š”์ผ ํœด๋ฌด" + }, + "staff": 3, + "seats": 25, + "monthlyRevenue": 22000000, + "avgCheckSize": 10000, + "menu": [ + {"name": "์Œ€๊ตญ์ˆ˜", "price": 8500}, + {"name": "๋ถ„์งœ", "price": 9500}, + {"name": "๋ฐ˜๋ฏธ", "price": 6500}, + {"name": "์ƒ์ถ˜๊ถŒ", "price": 8000} + ], + "targetCustomers": "20-30๋Œ€, ํ•™์ƒ, ๊ฐ€์„ฑ๋น„ ์„ ํ˜ธ ๊ณ ๊ฐ", + "strongPoints": "๊ฐ€์„ฑ๋น„, ๋น ๋ฅธ ์„œ๋น™, ๋ฐฐ๋‹ฌ ๊ฐ€๋Šฅ", + "locationCharacter": "์„œ๋ฉด ์ƒ๊ถŒ, ์ ์‹ฌยท์ €๋… ๊ท ๋“ฑ" +} +``` + +--- + +## 14. Mexican Restaurant (๋ฉ•์‹œ์ฝ” ์Œ์‹์ ) + +### 14-1. ํƒ€์ฝ”๋ฒจ +```json +{ + "storeId": "MX001", + "storeName": "ํƒ€์ฝ”๋ฒจ", + "category": "mexican_restaurant", + "categoryKo": "๋ฉ•์‹œ์ฝ” ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "๊ฐ•๋‚จ๊ตฌ", + "address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ๊ฐ•๋‚จ๋Œ€๋กœ 123" + }, + "businessHours": { + "weekday": "10:00-23:00", + "weekend": "10:00-23:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 6, + "seats": 40, + "monthlyRevenue": 52000000, + "avgCheckSize": 12000, + "menu": [ + {"name": "ํฌ๋Ÿฐ์น˜๋žฉ ์Šˆํ”„๋ฆผ", "price": 7500}, + {"name": "์น˜์ฆˆ ํ€˜์‚ฌ๋””์•„", "price": 6500}, + {"name": "๋ถ€๋ฆฌ๋˜", "price": 8000}, + {"name": "๋‚˜์ดˆ์Šค", "price": 5500} + ], + "targetCustomers": "10-30๋Œ€, ํ•™์ƒ, ์ง์žฅ์ธ", + "strongPoints": "๋ธŒ๋žœ๋“œ ์ธ์ง€๋„, ํ•ฉ๋ฆฌ์  ๊ฐ€๊ฒฉ, ๋…ํŠนํ•œ ๋ฉ”๋‰ด", + "locationCharacter": "๊ฐ•๋‚จ์—ญ ์—ญ์„ธ๊ถŒ, ์ ์‹ฌยท์ €๋… ํ”ผํฌํƒ€์ž„" +} +``` + +### 14-2. ๊ตฌ์Šคํ† ํƒ€์ฝ” +```json +{ + "storeId": "MX002", + "storeName": "๊ตฌ์Šคํ† ํƒ€์ฝ”", + "category": "mexican_restaurant", + "categoryKo": "๋ฉ•์‹œ์ฝ” ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "์ดํƒœ์›", + "address": "์„œ์šธ์‹œ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 456" + }, + "businessHours": { + "weekday": "11:30-22:00", + "weekend": "11:30-23:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 5, + "seats": 35, + "monthlyRevenue": 42000000, + "avgCheckSize": 22000, + "menu": [ + {"name": "ํƒ€์ฝ” ์„ธํŠธ", "price": 18000}, + {"name": "ํ€˜์‚ฌ๋””์•„", "price": 16000}, + {"name": "๋ถ€๋ฆฌ๋˜", "price": 15000}, + {"name": "๋งˆ๋ฅด๊ฐ€๋ฆฌํƒ€", "price": 12000} + ], + "targetCustomers": "20-40๋Œ€, ์™ธ๊ตญ์ธ, ์ˆ ์ž๋ฆฌ", + "strongPoints": "์ •ํ†ต ๋ฉ•์‹œ์นธ, ๋‹ค์–‘ํ•œ ์ˆ , ๋ถ„์œ„๊ธฐ", + "locationCharacter": "์ดํƒœ์› ํ•ซํ”Œ๋ ˆ์ด์Šค, ์ €๋… ๋งค์ถœ 80%" +} +``` + +### 14-3. ์—˜๋ณธ์ดˆ +```json +{ + "storeId": "MX003", + "storeName": "์—˜๋ณธ์ดˆ", + "category": "mexican_restaurant", + "categoryKo": "๋ฉ•์‹œ์ฝ” ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "ํ™๋Œ€", + "address": "์„œ์šธ์‹œ ๋งˆํฌ๊ตฌ ํ™๋Œ€์ž…๊ตฌ์—ญ 789" + }, + "businessHours": { + "weekday": "11:00-22:00", + "weekend": "11:00-23:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 35000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "๋ถ€๋ฆฌ๋˜", "price": 12000}, + {"name": "๋‚˜์ดˆ์Šค", "price": 10000}, + {"name": "ํ€˜์‚ฌ๋””์•„", "price": 11000}, + {"name": "ํƒ€์ฝ” ์„ธํŠธ", "price": 15000} + ], + "targetCustomers": "20-30๋Œ€, ๋Œ€ํ•™์ƒ, ๋ฐ์ดํŠธ", + "strongPoints": "๊ฐ€์„ฑ๋น„, SNS ๋งˆ์ผ€ํŒ…, ํ™๋Œ€ ์œ„์น˜", + "locationCharacter": "ํ™๋Œ€ ๋ฒˆํ™”๊ฐ€, ์ €๋…ยท์ฃผ๋ง ๋งค์ถœ ๋†’์Œ" +} +``` + +--- + +## 15. Seafood Restaurant (ํ•ด์‚ฐ๋ฌผ ์Œ์‹์ ) + +### 15-1. ๋ฐ”๋‹ค๋งˆ์„ +```json +{ + "storeId": "SF001", + "storeName": "๋ฐ”๋‹ค๋งˆ์„", + "category": "seafood_restaurant", + "categoryKo": "ํ•ด์‚ฐ๋ฌผ ์Œ์‹์ ", + "location": { + "city": "๋ถ€์‚ฐ", + "district": "๊ธฐ์žฅ๊ตฐ", + "address": "๋ถ€์‚ฐ์‹œ ๊ธฐ์žฅ๊ตฐ ์ผ๊ด‘๋ฉด ํ•ด์•ˆ๋กœ 123" + }, + "businessHours": { + "weekday": "10:00-22:00", + "weekend": "10:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 8, + "seats": 80, + "monthlyRevenue": 95000000, + "avgCheckSize": 45000, + "menu": [ + {"name": "ํšŒ (์ค‘)", "price": 60000}, + {"name": "ํšŒ (๋Œ€)", "price": 90000}, + {"name": "๋งค์šดํƒ•", "price": 15000}, + {"name": "ํ•ด๋ฌผ์ฐœ", "price": 50000} + ], + "targetCustomers": "๊ฐ€์กฑ, 40-60๋Œ€, ๊ด€๊ด‘๊ฐ", + "strongPoints": "์‹ ์„ ํ•œ ํ•ด์‚ฐ๋ฌผ, ์˜ค์…˜๋ทฐ, ์ฃผ์ฐจ ํŽธ๋ฆฌ", + "locationCharacter": "ํ•ด๋ณ€ ๊ด€๊ด‘์ง€, ์ฃผ๋ง ๋งค์ถœ ๋†’์Œ" +} +``` + +### 15-2. ํšŸ์ง‘ ์ˆ˜์‚ฐ +```json +{ + "storeId": "SF002", + "storeName": "ํšŸ์ง‘ ์ˆ˜์‚ฐ", + "category": "seafood_restaurant", + "categoryKo": "ํ•ด์‚ฐ๋ฌผ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "๋…ธ๋Ÿ‰์ง„", + "address": "์„œ์šธ์‹œ ๋™์ž‘๊ตฌ ๋…ธ๋Ÿ‰์ง„์ˆ˜์‚ฐ์‹œ์žฅ 456" + }, + "businessHours": { + "weekday": "09:00-23:00", + "weekend": "09:00-24:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 68000000, + "avgCheckSize": 38000, + "menu": [ + {"name": "ํšŒ (์†Œ)", "price": 40000}, + {"name": "ํšŒ (์ค‘)", "price": 60000}, + {"name": "์ดˆ๋ฐฅ", "price": 25000}, + {"name": "๋งค์šดํƒ•", "price": 12000} + ], + "targetCustomers": "์ „ ์—ฐ๋ น๋Œ€, ํšŒ์‹, ๋ชจ์ž„", + "strongPoints": "์‹œ์žฅ ๋‚ด ์œ„์น˜, ์‹ ์„ ํ•จ, ํ•ฉ๋ฆฌ์  ๊ฐ€๊ฒฉ", + "locationCharacter": "๋…ธ๋Ÿ‰์ง„์ˆ˜์‚ฐ์‹œ์žฅ, ์ €๋… ๋งค์ถœ ๋†’์Œ" +} +``` + +### 15-3. ์–ด์ดŒ +```json +{ + "storeId": "SF003", + "storeName": "์–ด์ดŒ", + "category": "seafood_restaurant", + "categoryKo": "ํ•ด์‚ฐ๋ฌผ ์Œ์‹์ ", + "location": { + "city": "์ธ์ฒœ", + "district": "๋‚จ๊ตฌ", + "address": "์ธ์ฒœ์‹œ ๋‚จ๊ตฌ ์†Œ๋ž˜ํฌ๊ตฌ 789" + }, + "businessHours": { + "weekday": "10:00-22:00", + "weekend": "10:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 6, + "seats": 60, + "monthlyRevenue": 72000000, + "avgCheckSize": 42000, + "menu": [ + {"name": "๊ฝƒ๊ฒŒํƒ•", "price": 40000}, + {"name": "์กฐ๊ฐœ๊ตฌ์ด", "price": 30000}, + {"name": "ํšŒ ์ •์‹", "price": 35000}, + {"name": "์นผ๊ตญ์ˆ˜", "price": 8000} + ], + "targetCustomers": "๊ฐ€์กฑ, ๊ด€๊ด‘๊ฐ, 40-60๋Œ€", + "strongPoints": "ํฌ๊ตฌ ์ง์†ก, ์‹ ์„ ํ•œ ์žฌ๋ฃŒ, ๋‹ค์–‘ํ•œ ๋ฉ”๋‰ด", + "locationCharacter": "์†Œ๋ž˜ํฌ๊ตฌ ๊ด€๊ด‘์ง€, ์ฃผ๋ง ์„ฑ์ˆ˜๊ธฐ" +} +``` + +--- + +## 16. Dessert Shop (๋””์ €ํŠธ ์ „๋ฌธ์ ) + +### 16-1. ์„ค๋น™ +```json +{ + "storeId": "DS001", + "storeName": "์„ค๋น™", + "category": "dessert_shop", + "categoryKo": "๋””์ €ํŠธ ์ „๋ฌธ์ ", + "location": { + "city": "์„œ์šธ", + "district": "๋ช…๋™", + "address": "์„œ์šธ์‹œ ์ค‘๊ตฌ ๋ช…๋™๊ธธ 123" + }, + "businessHours": { + "weekday": "11:00-23:00", + "weekend": "11:00-24:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 6, + "seats": 50, + "monthlyRevenue": 58000000, + "avgCheckSize": 14000, + "menu": [ + {"name": "์ธ์ ˆ๋ฏธ ์„ค๋น™", "price": 14000}, + {"name": "๋ง๊ณ  ์„ค๋น™", "price": 16000}, + {"name": "ํŒฅ๋น™์ˆ˜", "price": 12000}, + {"name": "์Œ๋ฃŒ", "price": 6000} + ], + "targetCustomers": "๊ด€๊ด‘๊ฐ, 20-30๋Œ€, ์™ธ๊ตญ์ธ", + "strongPoints": "๋ธŒ๋žœ๋“œ ์ธ์ง€๋„, ํ•œ๊ตญ ์ „ํ†ต ๋””์ €ํŠธ, ๊ด€๊ด‘์ง€ ์œ„์น˜", + "locationCharacter": "๋ช…๋™ ๊ด€๊ด‘์ง€, ์—ฌ๋ฆ„ ๋งค์ถœ ์ง‘์ค‘" +} +``` + +### 16-2. ๋งˆ์นด๋กฑํ•˜์šฐ์Šค +```json +{ + "storeId": "DS002", + "storeName": "๋งˆ์นด๋กฑํ•˜์šฐ์Šค", + "category": "dessert_shop", + "categoryKo": "๋””์ €ํŠธ ์ „๋ฌธ์ ", + "location": { + "city": "์„œ์šธ", + "district": "์ฒญ๋‹ด๋™", + "address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์ฒญ๋‹ด๋™ 456" + }, + "businessHours": { + "weekday": "10:00-21:00", + "weekend": "10:00-21:00", + "holiday": "๋งค์ฃผ ์›”์š”์ผ ํœด๋ฌด" + }, + "staff": 3, + "seats": 15, + "monthlyRevenue": 32000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "๋งˆ์นด๋กฑ 5๊ฐœ", "price": 15000}, + {"name": "๋งˆ์นด๋กฑ 10๊ฐœ", "price": 28000}, + {"name": "์ผ€์ดํฌ", "price": 40000}, + {"name": "์Œ๋ฃŒ", "price": 7000} + ], + "targetCustomers": "20-40๋Œ€ ์—ฌ์„ฑ, ์„ ๋ฌผ ๊ตฌ๋งค ๊ณ ๊ฐ", + "strongPoints": "ํ”„๋ฆฌ๋ฏธ์—„ ๋””์ €ํŠธ, ๊ฐ์„ฑ ํฌ์žฅ, SNS ๋งˆ์ผ€ํŒ…", + "locationCharacter": "์ฒญ๋‹ด๋™ ๊ณ ๊ธ‰ ์ƒ๊ถŒ, ์ฃผ๋ง ๋งค์ถœ ๋†’์Œ" +} +``` + +### 16-3. ์™€ํ”Œ๋Œ€ํ•™ +```json +{ + "storeId": "DS003", + "storeName": "์™€ํ”Œ๋Œ€ํ•™", + "category": "dessert_shop", + "categoryKo": "๋””์ €ํŠธ ์ „๋ฌธ์ ", + "location": { + "city": "์„œ์šธ", + "district": "์‹ ์ดŒ", + "address": "์„œ์šธ์‹œ ์„œ๋Œ€๋ฌธ๊ตฌ ์‹ ์ดŒ์—ญ 789" + }, + "businessHours": { + "weekday": "10:00-23:00", + "weekend": "10:00-24:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 28000000, + "avgCheckSize": 9000, + "menu": [ + {"name": "๋ฒจ๊ธฐ์— ์™€ํ”Œ", "price": 6500}, + {"name": "๋”ธ๊ธฐ ์™€ํ”Œ", "price": 8500}, + {"name": "์•„์ด์Šคํฌ๋ฆผ ์™€ํ”Œ", "price": 9500}, + {"name": "์Œ๋ฃŒ", "price": 5000} + ], + "targetCustomers": "๋Œ€ํ•™์ƒ, 10-20๋Œ€, ๋ฐ์ดํŠธ", + "strongPoints": "๊ฐ€์„ฑ๋น„, ๋Œ€ํ•™๊ฐ€ ์œ„์น˜, ๋‹ค์–‘ํ•œ ํ† ํ•‘", + "locationCharacter": "์‹ ์ดŒ ๋Œ€ํ•™๊ฐ€, ์ €๋… ๋งค์ถœ ๋†’์Œ" +} +``` + +--- + +## 17. Brunch Restaurant (๋ธŒ๋Ÿฐ์น˜ ์ „๋ฌธ์ ) + +### 17-1. ์—˜๋ฆฌ์ œ +```json +{ + "storeId": "BR001", + "storeName": "์—˜๋ฆฌ์ œ", + "category": "brunch_restaurant", + "categoryKo": "๋ธŒ๋Ÿฐ์น˜ ์ „๋ฌธ์ ", + "location": { + "city": "์„œ์šธ", + "district": "์—ฐ๋‚จ๋™", + "address": "์„œ์šธ์‹œ ๋งˆํฌ๊ตฌ ์—ฐ๋‚จ๋™ 123" + }, + "businessHours": { + "weekday": "09:00-17:00", + "weekend": "09:00-18:00", + "holiday": "๋งค์ฃผ ์›”์š”์ผ ํœด๋ฌด" + }, + "staff": 5, + "seats": 40, + "monthlyRevenue": 42000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "์—๊ทธ๋ฒ ๋„ค๋”•ํŠธ", "price": 16000}, + {"name": "ํŒฌ์ผ€์ดํฌ", "price": 14000}, + {"name": "์•„๋ณด์นด๋„ ํ† ์ŠคํŠธ", "price": 15000}, + {"name": "๋ธŒ๋Ÿฐ์น˜ ์„ธํŠธ", "price": 22000} + ], + "targetCustomers": "20-30๋Œ€ ์—ฌ์„ฑ, ๋ธŒ๋Ÿฐ์น˜ ์• ํ˜ธ๊ฐ€", + "strongPoints": "๊ฐ์„ฑ ์ธํ…Œ๋ฆฌ์–ด, ์ธ์Šคํƒ€๊ทธ๋ž˜๋จธ๋ธ”, ์›จ์ดํŒ… ๋ง›์ง‘", + "locationCharacter": "์—ฐ๋‚จ๋™ ํ•ซํ”Œ๋ ˆ์ด์Šค, ์ฃผ๋ง ์›จ์ดํŒ… 2์‹œ๊ฐ„" +} +``` + +### 17-2. ์—๊ทธ์Šฌ๋Ÿฟ +```json +{ + "storeId": "BR002", + "storeName": "์—๊ทธ์Šฌ๋Ÿฟ", + "category": "brunch_restaurant", + "categoryKo": "๋ธŒ๋Ÿฐ์น˜ ์ „๋ฌธ์ ", + "location": { + "city": "์„œ์šธ", + "district": "์ž ์‹ค", + "address": "์„œ์šธ์‹œ ์†กํŒŒ๊ตฌ ์ž ์‹ค ๋กฏ๋ฐ์›”๋“œ๋ชฐ 456" + }, + "businessHours": { + "weekday": "10:00-22:00", + "weekend": "10:00-22:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 8, + "seats": 60, + "monthlyRevenue": 95000000, + "avgCheckSize": 22000, + "menu": [ + {"name": "์—๊ทธ์Šฌ๋Ÿฟ", "price": 14000}, + {"name": "ํŽ˜์–ดํŒฉ์Šค", "price": 16000}, + {"name": "๋ฒ ์ด์ปจ ์—๊ทธ", "price": 15000}, + {"name": "์Œ๋ฃŒ", "price": 5000} + ], + "targetCustomers": "20-40๋Œ€, ๊ฐ€์กฑ, ์‡ผํ•‘ ๊ณ ๊ฐ", + "strongPoints": "๋ธŒ๋žœ๋“œ ์ธ์ง€๋„, ์‡ผํ•‘๋ชฐ ์œ„์น˜, ๋Œ€๊ธฐ ์‹œ์Šคํ…œ", + "locationCharacter": "๋กฏ๋ฐ์›”๋“œ๋ชฐ, ์ฃผ๋ง ์˜ค์ „ ์›จ์ดํŒ…" +} +``` + +### 17-3. ๋”ํ”Œ๋ ˆ์ดํŠธ +```json +{ + "storeId": "BR003", + "storeName": "๋”ํ”Œ๋ ˆ์ดํŠธ", + "category": "brunch_restaurant", + "categoryKo": "๋ธŒ๋Ÿฐ์น˜ ์ „๋ฌธ์ ", + "location": { + "city": "๋ถ€์‚ฐ", + "district": "ํ•ด์šด๋Œ€๊ตฌ", + "address": "๋ถ€์‚ฐ์‹œ ํ•ด์šด๋Œ€๊ตฌ ์„ผํ…€์‹œํ‹ฐ 789" + }, + "businessHours": { + "weekday": "09:00-17:00", + "weekend": "09:00-18:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 6, + "seats": 50, + "monthlyRevenue": 52000000, + "avgCheckSize": 20000, + "menu": [ + {"name": "์˜ฌ๋ฐ์ด ๋ธŒ๋ ‰ํผ์ŠคํŠธ", "price": 18000}, + {"name": "ํŒฌ์ผ€์ดํฌ", "price": 15000}, + {"name": "์ƒ๋Ÿฌ๋“œ", "price": 16000}, + {"name": "์ปคํ”ผ", "price": 6000} + ], + "targetCustomers": "20-40๋Œ€, ๊ฐ€์กฑ, ์ฃผ๋ง ๋ธŒ๋Ÿฐ์น˜", + "strongPoints": "๋„“์€ ๊ณต๊ฐ„, ์˜ค์…˜๋ทฐ, ๋‹ค์–‘ํ•œ ๋ฉ”๋‰ด", + "locationCharacter": "์„ผํ…€์‹œํ‹ฐ ์ƒ๊ถŒ, ์ฃผ๋ง ์˜ค์ „ ์ง‘์ค‘" +} +``` + +--- + +## 18. Vegetarian Restaurant (์ฑ„์‹ ์Œ์‹์ ) + +### 18-1. ํ‘ธ๋“œํ—Œํ„ฐ์Šค +```json +{ + "storeId": "VG001", + "storeName": "ํ‘ธ๋“œํ—Œํ„ฐ์Šค", + "category": "vegetarian_restaurant", + "categoryKo": "์ฑ„์‹ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "์ดํƒœ์›", + "address": "์„œ์šธ์‹œ ์šฉ์‚ฐ๊ตฌ ์ดํƒœ์›๋กœ 123" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "๋งค์ฃผ ์›”์š”์ผ ํœด๋ฌด" + }, + "staff": 4, + "seats": 30, + "monthlyRevenue": 32000000, + "avgCheckSize": 16000, + "menu": [ + {"name": "๋น„๊ฑด ๋ฒ„๊ฑฐ", "price": 14000}, + {"name": "ํ€ด๋…ธ์•„ ์ƒ๋Ÿฌ๋“œ", "price": 13000}, + {"name": "๋ฒ ์ง€ ์ปค๋ฆฌ", "price": 15000}, + {"name": "์Šค๋ฌด๋””", "price": 8000} + ], + "targetCustomers": "20-40๋Œ€, ๋น„๊ฑดยท์ฑ„์‹์ฃผ์˜์ž, ๊ฑด๊ฐ• ์ง€ํ–ฅ", + "strongPoints": "100% ๋น„๊ฑด, ์œ ๊ธฐ๋† ์žฌ๋ฃŒ, ๋‹ค์–‘ํ•œ ๋ฉ”๋‰ด", + "locationCharacter": "์ดํƒœ์› ๋‹ค๋ฌธํ™” ์ƒ๊ถŒ, ์ ์‹ฌ ๋งค์ถœ ๋†’์Œ" +} +``` + +### 18-2. ๊ทธ๋ฆฐํ…Œ์ด๋ธ” +```json +{ + "storeId": "VG002", + "storeName": "๊ทธ๋ฆฐํ…Œ์ด๋ธ”", + "category": "vegetarian_restaurant", + "categoryKo": "์ฑ„์‹ ์Œ์‹์ ", + "location": { + "city": "์„œ์šธ", + "district": "๊ฐ•๋‚จ๊ตฌ", + "address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ์‹ ์‚ฌ๋™ 456" + }, + "businessHours": { + "weekday": "11:00-21:00", + "weekend": "11:00-21:00", + "holiday": "์—ฐ์ค‘๋ฌดํœด" + }, + "staff": 3, + "seats": 25, + "monthlyRevenue": 28000000, + "avgCheckSize": 18000, + "menu": [ + {"name": "์ฑ„์‹ ์ •์‹", "price": 16000}, + {"name": "ํ˜„๋ฏธ๋ฐฅ ๋„์‹œ๋ฝ", "price": 12000}, + {"name": "ํ…œํŽ˜ ์Šคํ…Œ์ดํฌ", "price": 19000}, + {"name": "์ƒ์ฑ„์†Œ ์ฃผ์Šค", "price": 9000} + ], + "targetCustomers": "30-50๋Œ€, ๊ฑด๊ฐ• ๊ด€๋ฆฌ ๊ณ ๊ฐ, ์š”๊ฐ€์กฑ", + "strongPoints": "์ •๊ฐˆํ•œ ํ•œ์‹ ์ฑ„์‹, ๊ฑด๊ฐ•์‹, ์˜ค๊ฐ€๋‹‰", + "locationCharacter": "์‹ ์‚ฌ๋™ ๊ฐ€๋กœ์ˆ˜๊ธธ, ์ ์‹ฌ ๋งค์ถœ ์ง‘์ค‘" +} +``` + +### 18-3. ๋น„ํ‹€์ฆˆ +```json +{ + "storeId": "VG003", + "storeName": "๋น„ํ‹€์ฆˆ", + "category": "vegan_restaurant", + "categoryKo": "๋น„๊ฑด ์Œ์‹์ ", + "location": { + "city": "๋ถ€์‚ฐ", + "district": "ํ•ด์šด๋Œ€๊ตฌ", + "address": "๋ถ€์‚ฐ์‹œ ํ•ด์šด๋Œ€๊ตฌ ์šฐ๋™ 789" + }, + "businessHours": { + "weekday": "10:00-20:00", + "weekend": "10:00-20:00", + "holiday": "๋งค์ฃผ ์ผ์š”์ผ ํœด๋ฌด" + }, + "staff": 2, + "seats": 20, + "monthlyRevenue": 18000000, + "avgCheckSize": 14000, + "menu": [ + {"name": "๋น„๊ฑด ์ƒŒ๋“œ์œ„์น˜", "price": 10000}, + {"name": "์ƒ๋Ÿฌ๋“œ ๋ณผ", "price": 12000}, + {"name": "์Šค๋ฌด๋”” ๋ณผ", "price": 11000}, + {"name": "์ปคํ”ผ", "price": 5000} + ], + "targetCustomers": "20-30๋Œ€, ๊ฑด๊ฐ• ์ง€ํ–ฅ, ๋น„๊ฑด", + "strongPoints": "100% ๋น„๊ฑด, ์‹ ์„ ํ•œ ์žฌ๋ฃŒ, ๊ฐ€์„ฑ๋น„", + "locationCharacter": "ํ•ด์šด๋Œ€ ์ฃผํƒ๊ฐ€, ์ ์‹ฌ ๋งค์ถœ ๋†’์Œ" +} +``` + +--- + +## ๋ฐ์ดํ„ฐ ํ™œ์šฉ ๊ฐ€์ด๋“œ + +### AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ์‹œ๋‚˜๋ฆฌ์˜ค ์˜ˆ์‹œ + +**์‹œ๋‚˜๋ฆฌ์˜ค 1: ์ €๋น„์šฉ ์ด๋ฒคํŠธ ์ถ”์ฒœ** +- ๋Œ€์ƒ: ๋™๋„ค์น˜ํ‚จ(CK003) - ์›” ๋งค์ถœ 22,000,000์› +- ๋ชฉ์ : ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜ +- AI ์ถ”์ฒœ: + - ํ‰์ผ ์˜คํ›„ 5-7์‹œ 20% ํ• ์ธ + - SNS ๋ฆฌ๋ทฐ ์ž‘์„ฑ ์‹œ ์น˜ํ‚จ๋ฌด + ์Œ๋ฃŒ ๋ฌด๋ฃŒ + - ์˜ˆ์ƒ ๋น„์šฉ: 250,000์› + - ์˜ˆ์ƒ ํšจ๊ณผ: +15% ์ฃผ๋ฌธ ์ฆ๊ฐ€ + +**์‹œ๋‚˜๋ฆฌ์˜ค 2: ์ค‘๋น„์šฉ ์ด๋ฒคํŠธ ์ถ”์ฒœ** +- ๋Œ€์ƒ: ๊ฐ์„ฑ๊ณต๊ฐ„(CF001) - ์›” ๋งค์ถœ 38,000,000์› +- ๋ชฉ์ : ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ +- AI ์ถ”์ฒœ: + - ๋””์ง€ํ„ธ ์Šคํƒฌํ”„ 10ํšŒ โ†’ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์ผ€์ดํฌ ๋ฌด๋ฃŒ + - ์ธ์Šคํƒ€๊ทธ๋žจ ์Šคํ† ๋ฆฌ ํƒœ๊ทธ โ†’ ์Œ๋ฃŒ 1์ž” ๋ฌด๋ฃŒ + - ์˜ˆ์ƒ ๋น„์šฉ: 1,500,000์› + - ์˜ˆ์ƒ ํšจ๊ณผ: ์žฌ๋ฐฉ๋ฌธ์œจ +30% + +**์‹œ๋‚˜๋ฆฌ์˜ค 3: ๊ณ ๋น„์šฉ ์ด๋ฒคํŠธ ์ถ”์ฒœ** +- ๋Œ€์ƒ: ํ•œ์šฐ๋งˆ์„(BBQ001) - ์›” ๋งค์ถœ 85,000,000์› +- ๋ชฉ์ : ๋งค์ถœ ์ฆ๋Œ€ +- AI ์ถ”์ฒœ: + - VIP ๊ณ ๊ฐ ์ดˆ๋Œ€ ํŠน๋ณ„ ๋””๋„ˆ (50๋ช… ํ•œ์ •) + - 1++๋“ฑ๊ธ‰ ํ•œ์šฐ ํŠน๊ฐ€ + ์™€์ธ ํŽ˜์–ด๋ง + - ์˜ˆ์ƒ ๋น„์šฉ: 5,000,000์› + - ์˜ˆ์ƒ ํšจ๊ณผ: VIP ๊ณ ๊ฐ ๋กœ์—ดํ‹ฐ ๊ฐ•ํ™”, ๋งค์ถœ +25% + +--- + +## ๋ฐ์ดํ„ฐ ํ†ต๊ณ„ + +**์ด ๋งค์žฅ ์ˆ˜**: 54๊ฐœ +**์—…์ข… ๋ถ„๋ฅ˜**: 18๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ +**์ง€์—ญ ๋ถ„ํฌ**: +- ์„œ์šธ: 34๊ฐœ (63%) +- ๋ถ€์‚ฐ: 8๊ฐœ (15%) +- ์ธ์ฒœ: 4๊ฐœ (7%) +- ๋Œ€์ „: 3๊ฐœ (6%) +- ๊ฒฝ๊ธฐ: 3๊ฐœ (6%) +- ๋Œ€๊ตฌ: 2๊ฐœ (4%) + +**๋งค์ถœ ๊ทœ๋ชจ ๋ถ„ํฌ**: +- ์ƒ์กด ๋‹จ๊ณ„ (1.5-3์ฒœ๋งŒ์›): 12๊ฐœ (22%) +- ์„ฑ์žฅ ๋‹จ๊ณ„ (3-6์ฒœ๋งŒ์›): 24๊ฐœ (44%) +- ์•ˆ์ • ๋‹จ๊ณ„ (6์ฒœ๋งŒ์› ์ด์ƒ): 18๊ฐœ (33%) + +**์ง์› ๊ทœ๋ชจ**: +- 1-3๋ช…: 20๊ฐœ (37%) +- 4-6๋ช…: 26๊ฐœ (48%) +- 7๋ช… ์ด์ƒ: 8๊ฐœ (15%) + +--- + +**์ฐธ๊ณ ์‚ฌํ•ญ**: +- ๋ชจ๋“  ๊ธˆ์•ก์€ ์›ํ™”(KRW) ๊ธฐ์ค€ +- ์›” ํ‰๊ท  ๋งค์ถœ์€ ์ตœ๊ทผ 6๊ฐœ์›” ํ‰๊ท ๊ฐ’ +- ๊ฐ๋‹จ๊ฐ€๋Š” 1์ธ ๊ธฐ์ค€ ํ‰๊ท  ์ง€์ถœ์•ก +- ์˜์—…์‹œ๊ฐ„์€ ํ‘œ์ค€ ์šด์˜ ์‹œ๊ฐ„์ด๋ฉฐ ๋ณ€๋™ ๊ฐ€๋Šฅ + +**๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์˜ˆ์ •**: 2025-11-21 diff --git a/design/aidata/์‹œ์ฆŒ๋ณ„_์ด๋ฒคํŠธ_์„ฑ๊ณผ_๋ฐ์ดํ„ฐ.md b/design/aidata/์‹œ์ฆŒ๋ณ„_์ด๋ฒคํŠธ_์„ฑ๊ณผ_๋ฐ์ดํ„ฐ.md new file mode 100644 index 0000000..c452513 --- /dev/null +++ b/design/aidata/์‹œ์ฆŒ๋ณ„_์ด๋ฒคํŠธ_์„ฑ๊ณผ_๋ฐ์ดํ„ฐ.md @@ -0,0 +1,564 @@ +# ์‹œ์ฆŒ๋ณ„ ์ด๋ฒคํŠธ ์„ฑ๊ณผ ๋ฐ์ดํ„ฐ + +**์ž‘์„ฑ์ผ**: 2025-01-21 +**๋ชฉ์ **: AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ์‹œ์Šคํ…œ ํ•™์Šต ๋ฐ์ดํ„ฐ +**๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ**: 3๊ฐœ ์‹œ์ฆŒ ์นดํ…Œ๊ณ ๋ฆฌ ร— 10๊ฐœ ์ƒ˜ํ”Œ = ์ด 30๊ฐœ + +--- + +## ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์„ค๋ช… + +๋ณธ ๋ฐ์ดํ„ฐ๋Š” AI ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ถ”์ฒœ ์‹œ์Šคํ…œ์˜ ํ•™์Šต ๋ฐ์ดํ„ฐ๋กœ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค. ๊ฐ ์ด๋ฒคํŠธ ์ƒ˜ํ”Œ์€ ์‹ค์ œ ์„ฑ๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ์œผ๋ฉฐ, ๋‹ค์Œ ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค: + +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ์‹ค์ œ ์ง„ํ–‰๋œ ์ด๋ฒคํŠธ๋ช… +- **์—…์ข…**: ์Œ์‹์ , ์นดํŽ˜, ๋ฒ ์ด์ปค๋ฆฌ, ์น˜ํ‚จ์ง‘ ๋“ฑ +- **์ง€์—ญ**: ์„œ์šธ, ๊ฒฝ๊ธฐ, ๋ถ€์‚ฐ ๋“ฑ ์ฃผ์š” ์ง€์—ญ +- **์‹œ์ฆŒ**: ์ง„ํ–‰ ์‹œ๊ธฐ (๋ช…์ ˆ, ๊ณ„์ ˆ, ๊ธฐ๋…์ผ ๋“ฑ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: online(์˜จ๋ผ์ธ) / offline(์˜คํ”„๋ผ์ธ) +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: low(์ €์˜ˆ์‚ฐ), medium(์ค‘์˜ˆ์‚ฐ), high(๊ณ ์˜ˆ์‚ฐ) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ œ๊ณตํ•œ ์ธ์„ผํ‹ฐ๋ธŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๊ณ ๊ฐ ์ฐธ์—ฌ ํ”„๋กœ์„ธ์Šค +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: ์ด๋ฒคํŠธ ์ฐธ์—ฌ ๊ณ ๊ฐ ์ˆ˜ +- **์‹ค์ œ ๋น„์šฉ**: ์ด ์ด๋ฒคํŠธ ์šด์˜ ๋น„์šฉ (์›) +- **์‹ค์ œ ROI**: ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  (%) +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: ์ด๋ฒคํŠธ ์‹œ์ž‘-์ข…๋ฃŒ์ผ + +--- + +## ์นดํ…Œ๊ณ ๋ฆฌ 1: ํ•œ๊ตญ ์ „ํ†ต ๋ช…์ ˆ & ๊ตญ๊ฐ€ ๊ณตํœด์ผ ์‹œ์ฆŒ + +### 1. ์„ค๋‚  ํŠน์„  ๋ฉ”๋‰ด ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ์„ค๋‚  ํ•œ์ • ๋–ก๊ตญ ์„ธํŠธ ํ• ์ธ +- **์—…์ข…**: ํ•œ์‹๋‹น +- **์ง€์—ญ**: ์„œ์šธ ๊ฐ•๋‚จ๊ตฌ +- **์‹œ์ฆŒ**: ์„ค๋‚  (1์›” ๋ง~2์›” ์ดˆ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๋–ก๊ตญ ์„ธํŠธ 20% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ์‹œ ์ž๋™ ์ ์šฉ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 285๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 1,850,000์› +- **์‹ค์ œ ROI**: 420% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-01-20 ~ 2024-02-05 +- **์„ฑ๊ณต ์š”์ธ**: ์ „ํ†ต ๋ช…์ ˆ ์Œ์‹ ์ˆ˜์š”, ๊ฐ€์กฑ ๋‹จ์œ„ ์™ธ์‹ ์ฆ๊ฐ€ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ๋ช…์ ˆ 3์ผ ์ „๋ถ€ํ„ฐ ์˜ˆ์•ฝ ์ฃผ๋ฌธ ์ง‘์ค‘, ํฌ์žฅ ์ฃผ๋ฌธ 45% + +### 2. ์ถ”์„ ์„ ๋ฌผ ์„ธํŠธ ์‚ฌ์ „ ์˜ˆ์•ฝ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ์ถ”์„ ํ•œ์ •์‹ ์„ ๋ฌผ ์„ธํŠธ ์˜จ๋ผ์ธ ์‚ฌ์ „ ์ฃผ๋ฌธ +- **์—…์ข…**: ํ•œ์‹๋‹น +- **์ง€์—ญ**: ๊ฒฝ๊ธฐ ์„ฑ๋‚จ์‹œ +- **์‹œ์ฆŒ**: ์ถ”์„ (9์›”) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: online +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: high +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์‚ฌ์ „ ์ฃผ๋ฌธ ์‹œ 15% ํ• ์ธ + ๋ฐฐ์†ก๋น„ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์˜จ๋ผ์ธ ์ฃผ๋ฌธ ํŽ˜์ด์ง€์—์„œ ์˜ˆ์•ฝ ๊ฒฐ์ œ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 420๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 5,200,000์› +- **์‹ค์ œ ROI**: 380% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-08-25 ~ 2024-09-10 +- **์„ฑ๊ณต ์š”์ธ**: ์„ ๋ฌผ ์ˆ˜์š”, ์‚ฌ์ „ ์˜ˆ์•ฝ์œผ๋กœ ์žฌ๊ณ  ๊ด€๋ฆฌ ์šฉ์ด +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์ง€์ธ ์ถ”์ฒœ ์ด๋ฒคํŠธ ๋ณ‘ํ–‰์œผ๋กœ ์‹ ๊ทœ ๊ณ ๊ฐ 35% ์œ ์ž… + +### 3. ํฌ๋ฆฌ์Šค๋งˆ์Šค ์ปคํ”Œ ์„ธํŠธ ํ”„๋กœ๋ชจ์…˜ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ํฌ๋ฆฌ์Šค๋งˆ์Šค ์ปคํ”Œ ๋””๋„ˆ ํŠน๊ฐ€ +- **์—…์ข…**: ์–‘์‹๋‹น +- **์ง€์—ญ**: ์„œ์šธ ํ™๋Œ€ +- **์‹œ์ฆŒ**: ํฌ๋ฆฌ์Šค๋งˆ์Šค (12์›”) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: high +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ปคํ”Œ ๋””๋„ˆ ์ฝ”์Šค 30% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ „ํ™”/์˜จ๋ผ์ธ ์˜ˆ์•ฝ ํ›„ ๋ฐฉ๋ฌธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 320๋ช… (160์ปคํ”Œ) +- **์‹ค์ œ ๋น„์šฉ**: 6,400,000์› +- **์‹ค์ œ ROI**: 285% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-12-20 ~ 2024-12-26 +- **์„ฑ๊ณต ์š”์ธ**: ์—ฐ์ธ ๋ฐ์ดํŠธ ์ˆ˜์š”, ํ”„๋ฆฌ๋ฏธ์—„ ๊ฒฝํ—˜ ์ œ๊ณต +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์ธ์Šคํƒ€๊ทธ๋žจ ๊ฐ์„ฑ ์ธํ…Œ๋ฆฌ์–ด๋กœ SNS ์ž๋ฐœ์  ํ™•์‚ฐ + +### 4. ์‹ ์ • ์ƒˆํ•ด ์ฒซ ๋ฐฉ๋ฌธ ์ฟ ํฐ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ์ƒˆํ•ด ์ฒซ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ์Œ๋ฃŒ ๋ฌด๋ฃŒ +- **์—…์ข…**: ์นดํŽ˜ +- **์ง€์—ญ**: ๋ถ€์‚ฐ ํ•ด์šด๋Œ€๊ตฌ +- **์‹œ์ฆŒ**: ์‹ ์ • (1์›” 1์ผ~3์ผ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: low +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์•„๋ฉ”๋ฆฌ์นด๋…ธ ๋ฌด๋ฃŒ ์ œ๊ณต +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ์‹œ ์ž๋™ ์ œ๊ณต +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 220๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 280,000์› +- **์‹ค์ œ ROI**: 550% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2025-01-01 ~ 2025-01-03 +- **์„ฑ๊ณต ์š”์ธ**: ์ƒˆํ•ด ์ฒซ๋‚  ํŠน๋ณ„ํ•œ ๊ฒฝํ—˜, SNS ๊ณต์œ  ์œ ๋„ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ํ•ด์šด๋Œ€ ํ•ด๋‹์ด ๋ฐฉ๋ฌธ๊ฐ ์œ ์ž… ํšจ๊ณผ + +### 5. ์–ด๋ฆฐ์ด๋‚  ๊ฐ€์กฑ ํŒจํ‚ค์ง€ ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ์–ด๋ฆฐ์ด๋‚  ํ‚ค์ฆˆ ๋ฉ”๋‰ด ๋ฌด๋ฃŒ + ๊ฐ€์กฑ ํ• ์ธ +- **์—…์ข…**: ํŒจ๋ฐ€๋ฆฌ ๋ ˆ์Šคํ† ๋ž‘ +- **์ง€์—ญ**: ๊ฒฝ๊ธฐ ์ˆ˜์›์‹œ +- **์‹œ์ฆŒ**: ์–ด๋ฆฐ์ด๋‚  (5์›” 5์ผ ์ „ํ›„) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์–ด๋ฆฐ์ด ๋ฉ”๋‰ด 1์ธ ๋ฌด๋ฃŒ + ์„ฑ์ธ ๋ฉ”๋‰ด 15% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๊ฐ€์กฑ ๋‹จ์œ„ ๋ฐฉ๋ฌธ ์‹œ ์ž๋™ ์ ์šฉ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 580๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 2,100,000์› +- **์‹ค์ œ ROI**: 485% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-05-03 ~ 2024-05-07 +- **์„ฑ๊ณต ์š”์ธ**: ํ™ฉ๊ธˆ์—ฐํœด, ๊ฐ€์กฑ ์™ธ์‹ ์ˆ˜์š” ํญ๋ฐœ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์–ด๋ฆฐ์ด ๊ฒฝํ’ˆ(์žฅ๋‚œ๊ฐ) ์ฆ์ •์œผ๋กœ ๋งŒ์กฑ๋„ ์ƒ์Šน + +### 6. ์–ด๋ฒ„์ด๋‚  SNS ์ธ์ฆ ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๋ถ€๋ชจ๋‹˜๊ณผ ํ•จ๊ป˜ ์ธ์ฆ์ƒท ์ด๋ฒคํŠธ +- **์—…์ข…**: ํ•œ์‹๋‹น +- **์ง€์—ญ**: ์„œ์šธ ๊ฐ•์„œ๊ตฌ +- **์‹œ์ฆŒ**: ์–ด๋ฒ„์ด๋‚  (5์›” 8์ผ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: online +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: low +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ธ์Šคํƒ€๊ทธ๋žจ ์ธ์ฆ ์‹œ ๋””์ €ํŠธ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋ถ€๋ชจ๋‹˜๊ณผ ์‹์‚ฌ ์‚ฌ์ง„ + ํ•ด์‹œํƒœ๊ทธ + ๋งค์žฅ ํƒœ๊ทธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 185๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 320,000์› +- **์‹ค์ œ ROI**: 520% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-05-05 ~ 2024-05-12 +- **์„ฑ๊ณต ์š”์ธ**: SNS ํ™•์‚ฐ, ๊ฐ€์กฑ ์™ธ์‹ ํŠธ๋ Œ๋“œ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์ธ์Šคํƒ€๊ทธ๋žจ ํŒ”๋กœ์›Œ 240๋ช… ์ฆ๊ฐ€ + +### 7. ํฌ๋ฆฌ์Šค๋งˆ์Šค ์˜จ๋ผ์ธ ์ฃผ๋ฌธ ํ• ์ธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ํฌ๋ฆฌ์Šค๋งˆ์Šค ํ™ˆํŒŒํ‹ฐ ์น˜ํ‚จ ๋ฐฐ๋‹ฌ ํŠน๊ฐ€ +- **์—…์ข…**: ์น˜ํ‚จ์ง‘ +- **์ง€์—ญ**: ์„œ์šธ ์†กํŒŒ๊ตฌ +- **์‹œ์ฆŒ**: ํฌ๋ฆฌ์Šค๋งˆ์Šค (12์›”) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: online +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 2๋งˆ๋ฆฌ ์ด์ƒ ์ฃผ๋ฌธ ์‹œ 25% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋ฐฐ๋‹ฌ์•ฑ์„ ํ†ตํ•œ ์˜จ๋ผ์ธ ์ฃผ๋ฌธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 340๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 1,680,000์› +- **์‹ค์ œ ROI**: 395% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-12-20 ~ 2024-12-25 +- **์„ฑ๊ณต ์š”์ธ**: ํ™ˆํŒŒํ‹ฐ ์ˆ˜์š”, ๋ฐฐ๋‹ฌ ํŽธ์˜์„ฑ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ๋ฐฐ๋‹ฌ์•ฑ ์ฟ ํฐ ์ถ”๊ฐ€ ๋ฐœํ–‰์œผ๋กœ ์‹ ๊ทœ ๊ณ ๊ฐ 40% ์œ ์ž… + +### 8. ๊ด‘๋ณต์ ˆ ์• ๊ตญ ํ…Œ๋งˆ ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๊ด‘๋ณต์ ˆ ํƒœ๊ทน๊ธฐ ์ธ์ฆ ํ• ์ธ +- **์—…์ข…**: ์นดํŽ˜ +- **์ง€์—ญ**: ์„œ์šธ ์ข…๋กœ๊ตฌ +- **์‹œ์ฆŒ**: ๊ด‘๋ณต์ ˆ (8์›” 15์ผ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: low +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ํƒœ๊ทน๊ธฐ ์˜ท/์†Œํ’ˆ ์ฐฉ์šฉ ์‹œ ์Œ๋ฃŒ 20% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ์‹œ ํƒœ๊ทน๊ธฐ ์ฐฉ์šฉ ํ™•์ธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 95๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 150,000์› +- **์‹ค์ œ ROI**: 380% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-08-15 ~ 2024-08-15 +- **์„ฑ๊ณต ์š”์ธ**: ์• ๊ตญ์‹ฌ ํ…Œ๋งˆ, SNS ํ™”์ œ์„ฑ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์ง€์—ญ ํŠน์„ฑ์ƒ ๊ด€๊ด‘๊ฐ ์œ ์ž… ํšจ๊ณผ + +### 9. ๊ฐœ์ฒœ์ ˆ ๊ฐ€์„ ํ•œ์ • ๋ฉ”๋‰ด ์ถœ์‹œ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๊ฐ€์„ ๋‹จํ’ ์‹œ์ฆŒ ์‹ ๋ฉ”๋‰ด ์‹œ์‹ ์ด๋ฒคํŠธ +- **์—…์ข…**: ๋ฒ ์ด์ปค๋ฆฌ ์นดํŽ˜ +- **์ง€์—ญ**: ๊ฒฝ๊ธฐ ๋‚จ์–‘์ฃผ์‹œ +- **์‹œ์ฆŒ**: ๊ฐœ์ฒœ์ ˆ (10์›” ์ดˆ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์‹ ๋ฉ”๋‰ด ๋ฐค ํฌ๋ฆผ๋นต ์‹œ์‹ + ๊ตฌ๋งค ์‹œ 15% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ์‹œ ์‹œ์‹ ํ›„ ๊ตฌ๋งค +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 420๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 1,250,000์› +- **์‹ค์ œ ROI**: 440% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-10-01 ~ 2024-10-05 +- **์„ฑ๊ณต ์š”์ธ**: ๊ฐ€์„ ์ œ์ฒ  ์žฌ๋ฃŒ, ๋‹จํ’ ๊ด€๊ด‘๊ฐ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ํฌ์žฅ ์„ ๋ฌผ์šฉ ๊ตฌ๋งค ๋น„์œจ 60% + +### 10. ํ•œ๊ธ€๋‚  ํ•œ๊ตญ์–ด ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ํ•œ๊ธ€๋‚  ๋ฉ”๋‰ด๋ช… ํ•œ๊ธ€๋กœ ๋งํ•˜๊ธฐ ์ด๋ฒคํŠธ +- **์—…์ข…**: ์นดํŽ˜ +- **์ง€์—ญ**: ์„œ์šธ ๊ฐ•๋‚จ๊ตฌ +- **์‹œ์ฆŒ**: ํ•œ๊ธ€๋‚  (10์›” 9์ผ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: low +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๋ฉ”๋‰ด๋ฅผ ํ•œ๊ธ€๋กœ ์ฃผ๋ฌธ ์‹œ ์‚ฌ์ด์ฆˆ ์—…๊ทธ๋ ˆ์ด๋“œ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ฃผ๋ฌธ ์‹œ ํ•œ๊ธ€ ๋ฉ”๋‰ด๋ช…์œผ๋กœ ๋งํ•˜๊ธฐ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 280๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 180,000์› +- **์‹ค์ œ ROI**: 610% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-10-09 ~ 2024-10-09 +- **์„ฑ๊ณต ์š”์ธ**: ์ฐธ์—ฌ ์žฅ๋ฒฝ ๋‚ฎ์Œ, ์žฌ๋ฏธ ์š”์†Œ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: SNS ์ธ์ฆ์ƒท ์ž๋ฐœ์  ๊ณต์œ  ๋‹ค์ˆ˜ + +--- + +## ์นดํ…Œ๊ณ ๋ฆฌ 2: ๊ณ„์ ˆ๋ณ„ ์‹œ์ฆŒ + +### 11. ๋ด„ ๋ฒš๊ฝƒ ์‹œ์ฆŒ ํฌ์žฅ ๋„์‹œ๋ฝ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๋ฒš๊ฝƒ๋†€์ด ํ”ผํฌ๋‹‰ ๋„์‹œ๋ฝ ์„ธํŠธ +- **์—…์ข…**: ํ•œ์‹๋‹น +- **์ง€์—ญ**: ์„œ์šธ ์—ฌ์˜๋„ +- **์‹œ์ฆŒ**: ๋ด„ (4์›” ๋ฒš๊ฝƒ ์‹œ์ฆŒ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 2์ธ ๋„์‹œ๋ฝ ์„ธํŠธ 20,000์› (์ •์ƒ๊ฐ€ ๋Œ€๋น„ 25% ํ• ์ธ) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ „ํ™” ์‚ฌ์ „ ์ฃผ๋ฌธ ๋˜๋Š” ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ตฌ๋งค +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 520๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 1,950,000์› +- **์‹ค์ œ ROI**: 450% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-04-01 ~ 2024-04-10 +- **์„ฑ๊ณต ์š”์ธ**: ๋ฒš๊ฝƒ ์ถ•์ œ ๊ด€๊ด‘๊ฐ, ์•ผ์™ธ ์‹์‚ฌ ์ˆ˜์š” +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์—ฌ์˜๋„ ๋ฒš๊ฝƒ ์ถ•์ œ ๊ธฐ๊ฐ„ ์ง‘์ค‘ ๋งค์ถœ + +### 12. ์—ฌ๋ฆ„ ๋ณด์–‘์‹ ๋ณต๋‚  ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ์‚ผ๊ณ„ํƒ• ๋ณต๋‚  ํŠน๊ฐ€ ํ”„๋กœ๋ชจ์…˜ +- **์—…์ข…**: ํ•œ์‹๋‹น +- **์ง€์—ญ**: ๊ฒฝ๊ธฐ ๊ณ ์–‘์‹œ +- **์‹œ์ฆŒ**: ์—ฌ๋ฆ„ (์ดˆ๋ณต/์ค‘๋ณต/๋ง๋ณต) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: low +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์‚ผ๊ณ„ํƒ• ์ฃผ๋ฌธ ์‹œ ์Œ๋ฃŒ์ˆ˜ ๋ฌด๋ฃŒ ์ œ๊ณต +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ์ฃผ๋ฌธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 385๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 420,000์› +- **์‹ค์ œ ROI**: 580% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-07-15 ~ 2024-08-14 (๋ณต๋‚  3์ผ๊ฐ„) +- **์„ฑ๊ณต ์š”์ธ**: ์ „ํ†ต ๋ณด์–‘์‹ ์ˆ˜์š”, ๋ณต๋‚  ๋ฌธํ™” +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์˜ˆ์•ฝ ๊ณ ๊ฐ 80%, ๋‹น์ผ ๋ฐฉ๋ฌธ 20% + +### 13. ๊ฐ€์„ ์ „์–ด ์ถ•์ œ ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๊ฐ€์„ ์ œ์ฒ  ์ „์–ด ๊ตฌ์ด ํ• ์ธ +- **์—…์ข…**: ํ•ด์‚ฐ๋ฌผ ์Œ์‹์  +- **์ง€์—ญ**: ๋ถ€์‚ฐ ๊ด‘์•ˆ๋ฆฌ +- **์‹œ์ฆŒ**: ๊ฐ€์„ (9์›”~10์›”) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ „์–ด ๊ตฌ์ด ์ •์‹ 30% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ์‹œ ์ž๋™ ์ ์šฉ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 450๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 2,350,000์› +- **์‹ค์ œ ROI**: 410% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-09-15 ~ 2024-10-31 +- **์„ฑ๊ณต ์š”์ธ**: ์ œ์ฒ  ์žฌ๋ฃŒ, ๊ฐ€์„ ๋‹จํ’ ๊ด€๊ด‘๊ฐ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์ฃผ๋ง ์˜ˆ์•ฝ ํ•„์ˆ˜, ํ‰์ผ ๋‹น์ผ ๊ฐ€๋Šฅ + +### 14. ๊ฒจ์šธ ๋”ฐ๋œปํ•œ ๋ฉ”๋‰ด ํ”„๋กœ๋ชจ์…˜ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๊ฒจ์šธ ํ•œํŒŒ ํŠน๋ณด ์ฐŒ๊ฐœ ์„ธํŠธ ํ• ์ธ +- **์—…์ข…**: ํ•œ์‹๋‹น +- **์ง€์—ญ**: ์„œ์šธ ๋งˆํฌ๊ตฌ +- **์‹œ์ฆŒ**: ๊ฒจ์šธ (12์›”~2์›” ํ•œํŒŒ ๊ธฐ๊ฐ„) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ฐŒ๊ฐœ๋ฅ˜ ์ฃผ๋ฌธ ์‹œ ๊ณต๊ธฐ๋ฐฅ ๋ฌด๋ฃŒ + 10% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ์ฃผ๋ฌธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 620๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 1,850,000์› +- **์‹ค์ œ ROI**: 520% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-12-15 ~ 2025-02-28 +- **์„ฑ๊ณต ์š”์ธ**: ํ•œํŒŒ ํŠน๋ณด ํƒ€์ด๋ฐ, ๋”ฐ๋œปํ•œ ์Œ์‹ ์ˆ˜์š” +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ๋‚ ์”จ๊ฐ€ ์ถ”์šธ์ˆ˜๋ก ๋งค์ถœ ์ฆ๊ฐ€ (๋‚ ์”จ ์—ฐ๋™ ๋งˆ์ผ€ํŒ…) + +### 15. ๋ด„ ์‹ ํ•™๊ธฐ ํ•™์ƒ ํ• ์ธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ์‹ ํ•™๊ธฐ ํ•™์ƒ ์‘์› ์นดํŽ˜ ์Œ๋ฃŒ ํ• ์ธ +- **์—…์ข…**: ์นดํŽ˜ +- **์ง€์—ญ**: ์„œ์šธ ์‹ ์ดŒ +- **์‹œ์ฆŒ**: ๋ด„ (3์›” ๊ฐœํ•™ ์‹œ์ฆŒ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: low +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ํ•™์ƒ์ฆ ์ œ์‹œ ์‹œ ์Œ๋ฃŒ 20% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ฃผ๋ฌธ ์‹œ ํ•™์ƒ์ฆ ์ œ์‹œ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 580๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 450,000์› +- **์‹ค์ œ ROI**: 620% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-03-01 ~ 2024-03-31 +- **์„ฑ๊ณต ์š”์ธ**: ๋Œ€ํ•™๊ฐ€ ์ƒ๊ถŒ, ์‹ ํ•™๊ธฐ ๋ชจ์ž„ ์ฆ๊ฐ€ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์Šคํ„ฐ๋”” ๊ทธ๋ฃน ๋‹จ์ฒด ํ• ์ธ ๋ณ‘ํ–‰ + +### 16. ์—ฌ๋ฆ„ ๋น™์ˆ˜ ์‹œ์ฆŒ SNS ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ์ธ์Šคํƒ€๊ทธ๋žจ ๋น™์ˆ˜ ์ธ์ฆ์ƒท ์ด๋ฒคํŠธ +- **์—…์ข…**: ์นดํŽ˜ +- **์ง€์—ญ**: ๊ฒฝ๊ธฐ ์˜์ •๋ถ€์‹œ +- **์‹œ์ฆŒ**: ์—ฌ๋ฆ„ (7์›”~8์›”) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: online +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: low +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ธ์Šคํƒ€๊ทธ๋žจ ์Šคํ† ๋ฆฌ ํƒœ๊ทธ ์‹œ ๋‹ค์Œ ๋ฐฉ๋ฌธ ์Œ๋ฃŒ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋น™์ˆ˜ ์‚ฌ์ง„ + ๋งค์žฅ ํƒœ๊ทธ + ํ•ด์‹œํƒœ๊ทธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 340๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 380,000์› +- **์‹ค์ œ ROI**: 560% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-07-01 ~ 2024-08-31 +- **์„ฑ๊ณต ์š”์ธ**: SNS ํ™•์‚ฐ, ์—ฌ๋ฆ„ ํ•œ์ • ๋ฉ”๋‰ด +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์ธ์Šคํƒ€๊ทธ๋žจ ํŒ”๋กœ์›Œ 420๋ช… ์ฆ๊ฐ€ + +### 17. ๊ฐ€์„ ๋‹จํ’ ์‹œ์ฆŒ ๋ฐฐ๋‹ฌ ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๋‹จํ’๋†€์ด ํ›„ ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ ํ• ์ธ +- **์—…์ข…**: ์น˜ํ‚จ์ง‘ +- **์ง€์—ญ**: ๊ฒฝ๊ธฐ ๊ฐ€ํ‰๊ตฐ +- **์‹œ์ฆŒ**: ๊ฐ€์„ (10์›”~11์›” ๋‹จํ’ ์‹œ์ฆŒ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: online +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 2๋งŒ์› ์ด์ƒ ์ฃผ๋ฌธ ์‹œ ๋ฐฐ๋‹ฌ๋น„ ๋ฌด๋ฃŒ + ์Œ๋ฃŒ 2๋ณ‘ ์ฆ์ • +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋ฐฐ๋‹ฌ์•ฑ ๋˜๋Š” ์ „ํ™” ์ฃผ๋ฌธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 280๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 980,000์› +- **์‹ค์ œ ROI**: 420% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-10-10 ~ 2024-11-15 +- **์„ฑ๊ณต ์š”์ธ**: ๋‹จํ’ ๊ด€๊ด‘๊ฐ ํƒ€๊ฒŸ, ํŽœ์…˜/์ˆ™๋ฐ• ์‹œ์„ค ๋ฐฐ๋‹ฌ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์ฃผ๋ง ์ฃผ๋ฌธ ์ง‘์ค‘ (๊ธˆ~์ผ 70%) + +### 18. ๊ฒจ์šธ ํฌ๋ฆฌ์Šค๋งˆ์Šค ๋””์ €ํŠธ ์ถœ์‹œ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ํฌ๋ฆฌ์Šค๋งˆ์Šค ํ•œ์ • ์ผ€์ดํฌ ์‚ฌ์ „ ์˜ˆ์•ฝ +- **์—…์ข…**: ๋ฒ ์ด์ปค๋ฆฌ +- **์ง€์—ญ**: ์„œ์šธ ๊ฐ•๋‚จ๊ตฌ +- **์‹œ์ฆŒ**: ๊ฒจ์šธ (12์›” ํฌ๋ฆฌ์Šค๋งˆ์Šค) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: online +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: high +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์‚ฌ์ „ ์˜ˆ์•ฝ ์‹œ 15% ํ• ์ธ + ๋ฌด๋ฃŒ ํ”ฝ์—… +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์˜จ๋ผ์ธ ์‚ฌ์ „ ์ฃผ๋ฌธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 520๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 4,800,000์› +- **์‹ค์ œ ROI**: 340% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-12-01 ~ 2024-12-24 +- **์„ฑ๊ณต ์š”์ธ**: ํฌ๋ฆฌ์Šค๋งˆ์Šค ์ผ€์ดํฌ ํ•„์ˆ˜ ์ˆ˜์š”, ์‚ฌ์ „ ์˜ˆ์•ฝ ์žฌ๊ณ  ๊ด€๋ฆฌ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: 12์›” 20์ผ ์ดํ›„ ์ฃผ๋ฌธ ๋งˆ๊ฐ + +### 19. ๋ด„ ์ƒˆํ•™๊ธฐ MT ๋‹จ์ฒด ์ฃผ๋ฌธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๋Œ€ํ•™์ƒ MT ๋‹จ์ฒด ์ฃผ๋ฌธ ํŠน๊ฐ€ +- **์—…์ข…**: ์น˜ํ‚จ์ง‘ +- **์ง€์—ญ**: ๊ฐ•์›๋„ ์ถ˜์ฒœ์‹œ +- **์‹œ์ฆŒ**: ๋ด„ (3์›”~4์›” MT ์‹œ์ฆŒ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 10๋งˆ๋ฆฌ ์ด์ƒ ์ฃผ๋ฌธ ์‹œ 20% ํ• ์ธ + ๋ฐฐ๋‹ฌ๋น„ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ „ํ™” ์‚ฌ์ „ ์˜ˆ์•ฝ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 180๋ช… (18๊ฑด ๋‹จ์ฒด ์ฃผ๋ฌธ) +- **์‹ค์ œ ๋น„์šฉ**: 1,520,000์› +- **์‹ค์ œ ROI**: 480% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-03-10 ~ 2024-04-30 +- **์„ฑ๊ณต ์š”์ธ**: MT ์‹œ์ฆŒ ์ˆ˜์š”, ๋‹จ์ฒด ํ• ์ธ ๋งค๋ ฅ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ํŽœ์…˜/๋ฏผ๋ฐ• ๋ฐฐ๋‹ฌ ์ง‘์ค‘ + +### 20. ์—ฌ๋ฆ„ ํœด๊ฐ€ ์‹œ์ฆŒ ํฌ์žฅ ํ• ์ธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ์—ฌ๋ฆ„ ํœด๊ฐ€ ํฌ์žฅ ์ฃผ๋ฌธ ํŠน๊ฐ€ +- **์—…์ข…**: ํ•œ์‹๋‹น +- **์ง€์—ญ**: ๊ฐ•์›๋„ ์†์ดˆ์‹œ +- **์‹œ์ฆŒ**: ์—ฌ๋ฆ„ (7์›”~8์›” ํœด๊ฐ€ ์‹œ์ฆŒ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ํฌ์žฅ ์ฃผ๋ฌธ ์‹œ 10% ํ• ์ธ + ๋ฐ˜์ฐฌ ์ถ”๊ฐ€ ์ฆ์ • +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ๋˜๋Š” ์ „ํ™” ์ฃผ๋ฌธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 680๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 2,150,000์› +- **์‹ค์ œ ROI**: 510% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-07-20 ~ 2024-08-25 +- **์„ฑ๊ณต ์š”์ธ**: ์†์ดˆ ๊ด€๊ด‘๊ฐ, ์ˆ™์†Œ ๋‚ด ์‹์‚ฌ ์ˆ˜์š” +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์ €๋… ์‹œ๊ฐ„๋Œ€ ์ฃผ๋ฌธ ์ง‘์ค‘ (17~20์‹œ) + +--- + +## ์นดํ…Œ๊ณ ๋ฆฌ 3: ์ƒ์—…์  ๊ธฐ๋…์ผ & ์›”๋ณ„ ํŠนํ™” ์‹œ์ฆŒ + +### 21. ๋ฐœ๋ Œํƒ€์ธ๋ฐ์ด ์ปคํ”Œ ์„ธํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๋ฐœ๋ Œํƒ€์ธ๋ฐ์ด ์ปคํ”Œ ๋””์ €ํŠธ ์„ธํŠธ +- **์—…์ข…**: ์นดํŽ˜ +- **์ง€์—ญ**: ์„œ์šธ ํ™๋Œ€ +- **์‹œ์ฆŒ**: ๋ฐœ๋ Œํƒ€์ธ๋ฐ์ด (2์›” 14์ผ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ปคํ”Œ ๋””์ €ํŠธ ์„ธํŠธ 25% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: 2์ธ ์ด์ƒ ๋ฐฉ๋ฌธ ์‹œ ์ž๋™ ์ ์šฉ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 380๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 1,450,000์› +- **์‹ค์ œ ROI**: 440% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-02-10 ~ 2024-02-14 +- **์„ฑ๊ณต ์š”์ธ**: ์—ฐ์ธ ๋ฐ์ดํŠธ ์ˆ˜์š”, ๊ฐ์„ฑ ๋งˆ์ผ€ํŒ… +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์ธ์Šคํƒ€๊ทธ๋žจ ๊ฐ์„ฑ ์„ธํŒ…์œผ๋กœ SNS ํ™•์‚ฐ + +### 22. ํ™”์ดํŠธ๋ฐ์ด ๋‚จ์„ฑ ๊ณ ๊ฐ ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ํ™”์ดํŠธ๋ฐ์ด ๋‚จ์„ฑ ๊ณ ๊ฐ ์‚ฌํƒ• ์ฆ์ • +- **์—…์ข…**: ๋ฒ ์ด์ปค๋ฆฌ +- **์ง€์—ญ**: ๊ฒฝ๊ธฐ ๋ถ„๋‹น๊ตฌ +- **์‹œ์ฆŒ**: ํ™”์ดํŠธ๋ฐ์ด (3์›” 14์ผ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: low +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๋‚จ์„ฑ ๊ณ ๊ฐ ์ผ€์ดํฌ ๊ตฌ๋งค ์‹œ ์‚ฌํƒ• ์„ธํŠธ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ตฌ๋งค +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 240๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 380,000์› +- **์‹ค์ œ ROI**: 520% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-03-10 ~ 2024-03-14 +- **์„ฑ๊ณต ์š”์ธ**: ๋‹ต๋ก€ ์„ ๋ฌผ ์ˆ˜์š”, ๋‚จ์„ฑ ํƒ€๊ฒŸ ๋งˆ์ผ€ํŒ… +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์‚ฌ์ „ ์˜ˆ์•ฝ ๊ณ ๊ฐ 60% + +### 23. ๋ธ”๋ž™๋ฐ์ด 1์ธ ๋ฉ”๋‰ด ํŠน๊ฐ€ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๋ธ”๋ž™๋ฐ์ด ์งœ์žฅ๋ฉด ํ• ์ธ ์ด๋ฒคํŠธ +- **์—…์ข…**: ์ค‘์‹๋‹น +- **์ง€์—ญ**: ์„œ์šธ ์‹ ์ดŒ +- **์‹œ์ฆŒ**: ๋ธ”๋ž™๋ฐ์ด (4์›” 14์ผ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: low +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์งœ์žฅ๋ฉด/์งฌ๋ฝ• 1์ธ ๋ฉ”๋‰ด 30% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: 1์ธ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ์ž๋™ ์ ์šฉ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 185๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 320,000์› +- **์‹ค์ œ ROI**: 470% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-04-14 ~ 2024-04-14 +- **์„ฑ๊ณต ์š”์ธ**: ์†”๋กœ ๊ณ ๊ฐ ๊ณต๊ฐ, ์žฌ๋ฏธ ์š”์†Œ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: SNS ๋ฐ”์ด๋Ÿด ํšจ๊ณผ (๋Œ€ํ•™์ƒ ํƒ€๊ฒŸ) + +### 24. ๋นผ๋นผ๋กœ๋ฐ์ด ํ•™์ƒ ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๋นผ๋นผ๋กœ๋ฐ์ด ๊ผฌ์น˜ ๋ฉ”๋‰ด ํ• ์ธ +- **์—…์ข…**: ๋ถ„์‹์ง‘ +- **์ง€์—ญ**: ์„œ์šธ ๊ฐ•์„œ๊ตฌ +- **์‹œ์ฆŒ**: ๋นผ๋นผ๋กœ๋ฐ์ด (11์›” 11์ผ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: low +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๋–ก๊ผฌ์น˜/์–ด๋ฌต ๋“ฑ ๊ธธ์ญ‰ํ•œ ๋ฉ”๋‰ด 20% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํ•™์ƒ์ฆ ์ œ์‹œ ์‹œ ์ ์šฉ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 320๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 280,000์› +- **์‹ค์ œ ROI**: 580% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-11-09 ~ 2024-11-11 +- **์„ฑ๊ณต ์š”์ธ**: ํ•™์ƒ ํƒ€๊ฒŸ, ์žฌ๋ฏธ์žˆ๋Š” ์ปจ์…‰ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ํ•™๊ต ๊ทผ์ฒ˜ ์ƒ๊ถŒ ํšจ๊ณผ + +### 25. ๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด ํŒŒ๊ฒฉ ํ• ์ธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด 50% ํŒŒ๊ฒฉ ํ• ์ธ +- **์—…์ข…**: ์น˜ํ‚จ์ง‘ +- **์ง€์—ญ**: ๊ฒฝ๊ธฐ ์•ˆ์–‘์‹œ +- **์‹œ์ฆŒ**: ๋ธ”๋ž™ํ”„๋ผ์ด๋ฐ์ด (11์›” ๋„ท์งธ ์ฃผ ๊ธˆ์š”์ผ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: online +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: high +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์„ ์ฐฉ์ˆœ 50๋ช… ์น˜ํ‚จ 50% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋ฐฐ๋‹ฌ์•ฑ ์„ ์ฐฉ์ˆœ ์ฃผ๋ฌธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 50๋ช… (์กฐ๊ธฐ ๋งˆ๊ฐ) +- **์‹ค์ œ ๋น„์šฉ**: 850,000์› +- **์‹ค์ œ ROI**: 380% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-11-29 ~ 2024-11-29 +- **์„ฑ๊ณต ์š”์ธ**: ํฌ์†Œ์„ฑ ๋งˆ์ผ€ํŒ…, ํŒŒ๊ฒฉ ํ• ์ธ์œจ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: 2์‹œ๊ฐ„ ๋งŒ์— ์™„ํŒ, SNS ํ™”์ œ + +### 26. ์›”๋ณ„ ์ƒ์ผ ์ถ•ํ•˜ ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ์ƒ์ผ ๊ณ ๊ฐ ์ผ€์ดํฌ ๋ฌด๋ฃŒ ์ฆ์ • +- **์—…์ข…**: ๋ ˆ์Šคํ† ๋ž‘ +- **์ง€์—ญ**: ์„œ์šธ ์šฉ์‚ฐ๊ตฌ +- **์‹œ์ฆŒ**: ์—ฐ์ค‘ (๋งค์›”) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ƒ์ผ ๋‹น์ผ ๋ฐฉ๋ฌธ ์‹œ ๋ฏธ๋‹ˆ ์ผ€์ดํฌ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์‹ ๋ถ„์ฆ ์ œ์‹œ ๋˜๋Š” ์‚ฌ์ „ ์˜ˆ์•ฝ ์‹œ ์ƒ์ผ ์ •๋ณด ์ œ๊ณต +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 420๋ช… (์›” 35๋ช… ํ‰๊ท ) +- **์‹ค์ œ ๋น„์šฉ**: 1,680,000์› (์—ฐ๊ฐ„) +- **์‹ค์ œ ROI**: 550% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-01-01 ~ 2024-12-31 +- **์„ฑ๊ณต ์š”์ธ**: ํŠน๋ณ„ํ•œ ๋‚  ๊ฒฝํ—˜, ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์ƒ์ผ ๊ณ ๊ฐ์˜ ์žฌ๋ฐฉ๋ฌธ์œจ 70% + +### 27. ์‚ผ๊ฒน์‚ด๋ฐ์ด ๊ณ ๊นƒ์ง‘ ํ”„๋กœ๋ชจ์…˜ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: 3์›” 3์ผ ์‚ผ๊ฒน์‚ด ๋ฌดํ•œ๋ฆฌํ•„ +- **์—…์ข…**: ๊ณ ๊นƒ์ง‘ +- **์ง€์—ญ**: ์„œ์šธ ๊ฐ•๋‚จ๊ตฌ +- **์‹œ์ฆŒ**: ์‚ผ๊ฒน์‚ด๋ฐ์ด (3์›” 3์ผ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: high +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์‚ผ๊ฒน์‚ด ๋ฌดํ•œ๋ฆฌํ•„ ํŠน๊ฐ€ (1์ธ 19,900์›) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ (์˜ˆ์•ฝ ํ•„์ˆ˜) +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 520๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 5,800,000์› +- **์‹ค์ œ ROI**: 320% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-03-03 ~ 2024-03-03 +- **์„ฑ๊ณต ์š”์ธ**: ๊ธฐ๋…์ผ ํŠนํ™”, ๋ฌดํ•œ๋ฆฌํ•„ ๋งค๋ ฅ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์˜ˆ์•ฝ 1์ฃผ ์ „ ๋งˆ๊ฐ + +### 28. ์น˜ํ‚จ๋ฐ์ด ๋ฐฐ๋‹ฌ ํ”„๋กœ๋ชจ์…˜ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๋งค์›” ์…‹์งธ ์ฃผ ๋ชฉ์š”์ผ ์น˜ํ‚จ ํ• ์ธ +- **์—…์ข…**: ์น˜ํ‚จ์ง‘ +- **์ง€์—ญ**: ๊ฒฝ๊ธฐ ์„ฑ๋‚จ์‹œ +- **์‹œ์ฆŒ**: ๋งค์›” ์…‹์งธ ์ฃผ ๋ชฉ์š”์ผ +- **์ด๋ฒคํŠธ ์œ ํ˜•**: online +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์น˜ํ‚จ ์ฃผ๋ฌธ ์‹œ ์Œ๋ฃŒ์ˆ˜ 2L ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋ฐฐ๋‹ฌ์•ฑ ๋˜๋Š” ์ „ํ™” ์ฃผ๋ฌธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 1,680๋ช… (์—ฐ๊ฐ„, ์›” 140๋ช… ํ‰๊ท ) +- **์‹ค์ œ ๋น„์šฉ**: 2,520,000์› (์—ฐ๊ฐ„) +- **์‹ค์ œ ROI**: 480% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-01-01 ~ 2024-12-31 +- **์„ฑ๊ณต ์š”์ธ**: ์ •๊ธฐ ์ด๋ฒคํŠธ๋กœ ๊ณ ๊ฐ ํ•™์Šต ํšจ๊ณผ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ๋ชฉ์š”์ผ ๋งค์ถœ 35% ์ฆ๊ฐ€ + +### 29. ์ปคํ”ผ๋ฐ์ด ์นดํŽ˜ ํ”„๋กœ๋ชจ์…˜ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: 10์›” 1์ผ ์ปคํ”ผ์˜ ๋‚  ์•„๋ฉ”๋ฆฌ์นด๋…ธ ๋ฐ˜๊ฐ’ +- **์—…์ข…**: ์นดํŽ˜ +- **์ง€์—ญ**: ๋ถ€์‚ฐ ์„œ๋ฉด +- **์‹œ์ฆŒ**: ์ปคํ”ผ์˜ ๋‚  (10์›” 1์ผ) +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์•„๋ฉ”๋ฆฌ์นด๋…ธ 50% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ์ฃผ๋ฌธ +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 680๋ช… +- **์‹ค์ œ ๋น„์šฉ**: 1,350,000์› +- **์‹ค์ œ ROI**: 520% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-10-01 ~ 2024-10-01 +- **์„ฑ๊ณต ์š”์ธ**: ์ปคํ”ผ ์• ํ˜ธ๊ฐ€ ํƒ€๊ฒŸ, ํŒŒ๊ฒฉ ํ• ์ธ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: 1์ธ ์ตœ๋Œ€ 2์ž” ์ œํ•œ + +### 30. ๋นต์˜ ๋‚  ๋ฒ ์ด์ปค๋ฆฌ ์ด๋ฒคํŠธ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๋งค์›” 3์ผ ๋นต 1+1 ์ด๋ฒคํŠธ +- **์—…์ข…**: ๋ฒ ์ด์ปค๋ฆฌ +- **์ง€์—ญ**: ์„œ์šธ ๋งˆํฌ๊ตฌ +- **์‹œ์ฆŒ**: ๋งค์›” 3์ผ +- **์ด๋ฒคํŠธ ์œ ํ˜•**: offline +- **์˜ˆ์‚ฐ ๋ฒ”์œ„**: medium +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๋นต 1๊ฐœ ๊ตฌ๋งค ์‹œ 1๊ฐœ ์ถ”๊ฐ€ (๋™์ผ ์ƒํ’ˆ) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ตฌ๋งค +- **์‹ค์ œ ์ฐธ์—ฌ์ž ์ˆ˜**: 1,920๋ช… (์—ฐ๊ฐ„, ์›” 160๋ช… ํ‰๊ท ) +- **์‹ค์ œ ๋น„์šฉ**: 2,880,000์› (์—ฐ๊ฐ„) +- **์‹ค์ œ ROI**: 450% +- **์ง„ํ–‰ ๊ธฐ๊ฐ„**: 2024-01-03 ~ 2024-12-03 +- **์„ฑ๊ณต ์š”์ธ**: ์ •๊ธฐ ์ด๋ฒคํŠธ, 1+1 ๋งค๋ ฅ +- **์ฐธ๊ณ  ์‚ฌํ•ญ**: ์„ ์ฐฉ์ˆœ 200๊ฐœ ํ•œ์ • (๋งค์›”) + +--- + +## ๋ฐ์ดํ„ฐ ํ™œ์šฉ ๊ฐ€์ด๋“œ + +### AI ํ•™์Šต์„ ์œ„ํ•œ ์ฃผ์š” ํŒจํ„ด + +1. **์‹œ์ฆŒ๋ณ„ ํŠน์„ฑ** + - ๋ช…์ ˆ: ๊ฐ€์กฑ ๋‹จ์œ„, ์ „ํ†ต ์Œ์‹, ์„ ๋ฌผ ์ˆ˜์š” + - ๊ณ„์ ˆ: ๋‚ ์”จ ์—ฐ๋™, ์ œ์ฒ  ์žฌ๋ฃŒ, ๊ด€๊ด‘ ์ˆ˜์š” + - ๊ธฐ๋…์ผ: ํƒ€๊ฒŸ ์„ธ๋ถ„ํ™”, ๊ฐ์„ฑ ๋งˆ์ผ€ํŒ…, SNS ํ™œ์šฉ + +2. **์˜ˆ์‚ฐ๋ณ„ ROI ํŒจํ„ด** + - ์ €์˜ˆ์‚ฐ (30-50๋งŒ์›): ํ‰๊ท  ROI 550% (๋†’์€ ํšจ์œจ) + - ์ค‘์˜ˆ์‚ฐ (100-200๋งŒ์›): ํ‰๊ท  ROI 450% (์•ˆ์ •์ ) + - ๊ณ ์˜ˆ์‚ฐ (300๋งŒ์› ์ด์ƒ): ํ‰๊ท  ROI 350% (๋ธŒ๋žœ๋“œ ๊ฐ€์น˜ ์ƒ์Šน) + +3. **์˜จ๋ผ์ธ vs ์˜คํ”„๋ผ์ธ** + - ์˜จ๋ผ์ธ: ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์šฉ์ด, ์ Š์€ ์ธต ํƒ€๊ฒŸ, ROI ์ธก์ • ๋ช…ํ™• + - ์˜คํ”„๋ผ์ธ: ์ฒดํ—˜ ์ค‘์‹ฌ, ์ฆ‰๊ฐ ๋ณด์ƒ, ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ + +4. **์—…์ข…๋ณ„ ์„ฑ๊ณต ํŒจํ„ด** + - ํ•œ์‹๋‹น: ๋ช…์ ˆ/๊ณ„์ ˆ ์—ฐ๊ณ„, ์ „ํ†ต ๊ฐ€์น˜ + - ์นดํŽ˜: SNS ํ™œ์šฉ, ๊ฐ์„ฑ ๋งˆ์ผ€ํŒ…, ์ •๊ธฐ ์ด๋ฒคํŠธ + - ์น˜ํ‚จ์ง‘: ๋ฐฐ๋‹ฌ ์ตœ์ ํ™”, ์ •๊ธฐ ํ• ์ธ, ๋ฌด๋ฃŒ ์ฆ์ • + - ๋ฒ ์ด์ปค๋ฆฌ: ์‚ฌ์ „ ์˜ˆ์•ฝ, ํ•œ์ • ์ƒํ’ˆ, ์‹œ๊ฐ์  ๋งค๋ ฅ + +5. **์ฐธ์—ฌ ๋ฐฉ๋ฒ•๋ณ„ ํšจ๊ณผ** + - SNS ์ธ์ฆ: ๋ฐ”์ด๋Ÿด ํšจ๊ณผ, ๋‚ฎ์€ ๋น„์šฉ, ๋†’์€ ROI + - ์„ ์ฐฉ์ˆœ: ๊ธด๊ธ‰์„ฑ, ๋†’์€ ์ฐธ์—ฌ์œจ, ๋งค์ถœ ์ง‘์ค‘ + - ์ž๋™ ์ ์šฉ: ์ฐธ์—ฌ ์žฅ๋ฒฝ ๋‚ฎ์Œ, ๋งŒ์กฑ๋„ ๋†’์Œ + - ์‚ฌ์ „ ์˜ˆ์•ฝ: ์žฌ๊ณ  ๊ด€๋ฆฌ, ๊ณ„ํš์  ์šด์˜ + +### ๋ฐ์ดํ„ฐ ํ™•์žฅ ๋ฐฉํ–ฅ + +๋ณธ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๋Š” AI ์‹œ์Šคํ…œ์˜ ์ดˆ๊ธฐ ํ•™์Šต์šฉ์ž…๋‹ˆ๋‹ค. ์‹ค์ œ ์šด์˜ ์‹œ ๋‹ค์Œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ถ”๊ฐ€๋กœ ์ˆ˜์ง‘๋ฉ๋‹ˆ๋‹ค: + +- ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ +- ์‹ค์ œ ์„ฑ๊ณผ ๋ฐ์ดํ„ฐ (์ฐธ์—ฌ์ž, ๋น„์šฉ, ROI) +- ๊ณ ๊ฐ ํ”ผ๋“œ๋ฐฑ (๋งŒ์กฑ๋„, ์žฌ๋ฐฉ๋ฌธ์œจ) +- A/B ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ +- ์ง€์—ญ๋ณ„/์—…์ข…๋ณ„ ์„ธ๋ถ„ํ™” ๋ฐ์ดํ„ฐ + +--- + +**๋ฐ์ดํ„ฐ ๋ฒ„์ „**: 1.0 +**์ตœ์ข… ์ˆ˜์ •**: 2025-01-21 +**๋‹ค์Œ ์—…๋ฐ์ดํŠธ ์˜ˆ์ •**: ์‹ค์ œ ์„œ๋น„์Šค ์šด์˜ ํ›„ ๋ถ„๊ธฐ๋ณ„ diff --git a/design/aidata/์—…์ข…๋ณ„_์„ฑ๊ณต_์ด๋ฒคํŠธ_๋ฐ์ดํ„ฐ.md b/design/aidata/์—…์ข…๋ณ„_์„ฑ๊ณต_์ด๋ฒคํŠธ_๋ฐ์ดํ„ฐ.md new file mode 100644 index 0000000..35e9677 --- /dev/null +++ b/design/aidata/์—…์ข…๋ณ„_์„ฑ๊ณต_์ด๋ฒคํŠธ_๋ฐ์ดํ„ฐ.md @@ -0,0 +1,982 @@ +# ์—…์ข…๋ณ„ ์„ฑ๊ณต ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ + +**์ž‘์„ฑ์ผ**: 2025-10-21 +**๋ฒ„์ „**: 1.0 +**๋ชฉ์ **: AI ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ถ”์ฒœ ์‹œ์Šคํ…œ ํ•™์Šต ๋ฐ์ดํ„ฐ + +--- + +## ๊ฐœ์š” + +๋ณธ ๋ฌธ์„œ๋Š” ํ•œ๊ตญ ์™ธ์‹์—… ์†Œ์ƒ๊ณต์ธ์„ ์œ„ํ•œ ์—…์ข…๋ณ„ ์„ฑ๊ณต ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์‹ค์ œ ์„ฑ๊ณต ์‚ฌ๋ก€์™€ ํ•™์ˆ  ์—ฐ๊ตฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ์œผ๋ฉฐ, AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ์‹œ์Šคํ…œ์˜ ํ•™์Šต ๋ฐ์ดํ„ฐ๋กœ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค. + +### ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ + +๊ฐ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ๋Š” ๋‹ค์Œ ํ•ญ๋ชฉ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค: + +- **์—…์ข…๋ช…**: ์™ธ์‹์—… ์นดํ…Œ๊ณ ๋ฆฌ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: ๊ณ ๊ฐ์—๊ฒŒ ํ‘œ์‹œ๋˜๋Š” ์ด๋ฒคํŠธ๋ช… +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ํ• ์ธ, ์ฟ ํฐ, ๊ฒฝํ’ˆ, ์Šคํƒฌํ”„, ๋ฒˆ๋“ค, SNS, ์ฐฝ์˜์ , ๋ฐฐ๋‹ฌ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๊ณ ๊ฐ์—๊ฒŒ ์ œ๊ณต๋˜๋Š” ๊ตฌ์ฒด์  ํ˜œํƒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๊ณ ๊ฐ ์ฐธ์—ฌ ์ ˆ์ฐจ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €(25-30๋งŒ์›), ์ค‘(150-180๋งŒ์›), ๊ณ (500-600๋งŒ์›) +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„ ์ค‘ ์ฐธ์—ฌ ๊ณ ๊ฐ ์ˆ˜ +- **์˜ˆ์ƒ ๋น„์šฉ**: ์‹ค์ œ ์†Œ์š” ์˜ˆ์ƒ ๊ธˆ์•ก +- **์˜ˆ์ƒ ROI**: ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  (%) +- **์„ฑ๊ณต ์š”์ธ**: ์ด๋ฒคํŠธ ์„ฑ๊ณต ํ•ต์‹ฌ ์š”์†Œ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์ตœ์  ์‹คํ–‰ ์‹œ๊ธฐ + +### ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜ + +- ์‹ค์ œ ์„ฑ๊ณต ์‚ฌ๋ก€: ์ž๋‹ด์น˜ํ‚จ(+28.4%), ํฌ์น˜์น˜ํ‚จ(๋งค์ถœ 3๋ฐฐ), ํ™ˆํ”Œ๋Ÿฌ์Šค ๋‹น๋‹น์น˜ํ‚จ(710๋งŒํŒฉ), ์•„๋œจ๋ฒ (6๋…„ ์„ฑ์žฅ) +- ํ•™์ˆ  ์—ฐ๊ตฌ: ์ฒดํ—˜ ๋งˆ์ผ€ํŒ… ROI 250-300%, ๋ฆฌ๋ทฐ 1๊ฐœ๋‹น ์ฃผ๋ฌธ ํ™•๋ฅ  +5-7% +- ์—…๊ณ„ ๋ฒค์น˜๋งˆํฌ: ์ฐธ์—ฌ์œจ 15-35%, ์ „ํ™˜์œจ 15-40%, ๋งˆ์ผ€ํŒ… ROI 400%+ + +--- + +## 1. ํ•œ์‹๋‹น (Korean Restaurant) + +### 1-1. ์ ์‹ฌ ์‹œ๊ฐ„๋Œ€ ์„ธํŠธ ํ• ์ธ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ํ•œ์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์˜คํ”ผ์Šค ๋Ÿฐ์น˜ ํƒ€์ž„ ์„ธํŠธ 20% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์‹œ๊ฐ„๋Œ€๋ณ„ ํ• ์ธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 11์‹œ-14์‹œ ํŠน์ • ์„ธํŠธ ๋ฉ”๋‰ด 20% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€ ๋ฐฉ๋ฌธ ์‹œ ์ž๋™ ์ ์šฉ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 180๋ช… (์›” ๊ธฐ์ค€) +- **์˜ˆ์ƒ ๋น„์šฉ**: 280,000์› +- **์˜ˆ์ƒ ROI**: 350% +- **์„ฑ๊ณต ์š”์ธ**: + - ์ˆ˜์š” ํ‰์ค€ํ™” (๋น„์ˆ˜๊ธฐ ์‹œ๊ฐ„๋Œ€ ํ™œ์„ฑํ™”) + - ๋ช…ํ™•ํ•œ ํƒ€๊ฒŸํŒ… (์ง์žฅ์ธ ์ ์‹ฌ ์ˆ˜์š”) + - ์ฆ‰์‹œ ํ˜œํƒ (๋ณต์žกํ•œ ์ ˆ์ฐจ ์—†์Œ) +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘, ํŠนํžˆ ๋ด„/๊ฐ€์„ (๋‚ ์”จ ์ข‹์€ ์‹œ๊ธฐ) + +### 1-2. ๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค ๋ฆฌ๋ทฐ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ํ•œ์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๋ฆฌ๋ทฐ ์ž‘์„ฑํ•˜๊ณ  ์Œ๋ฃŒ ๋ฌด๋ฃŒ!" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ๋ฆฌ๋ทฐ ์ด๋ฒคํŠธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค ๋ฆฌ๋ทฐ ์ž‘์„ฑ ์‹œ ์Œ๋ฃŒ 1์ž” ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ โ†’ ์‹์‚ฌ ํ›„ ๋„ค์ด๋ฒ„ ๋ฆฌ๋ทฐ ์ž‘์„ฑ โ†’ ์ง์›์—๊ฒŒ ํ™”๋ฉด ์ œ์‹œ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 120๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 250,000์› (์Œ๋ฃŒ ์›๊ฐ€ 2,000์› ร— 120๋ช… + ํ™๋ณด) +- **์˜ˆ์ƒ ROI**: 280% +- **์„ฑ๊ณต ์š”์ธ**: + - ์˜จ๋ผ์ธ ํ‰ํŒ ๊ฐœ์„  (๋ฆฌ๋ทฐ 1๊ฐœ๋‹น ์ฃผ๋ฌธ ํ™•๋ฅ  +5-7%) + - ์ฆ‰์‹œ ๋ณด์ƒ (๋ฐฉ๋ฌธ ๋‹น์ผ ํ˜œํƒ) + - ๋‚ฎ์€ ์ฐธ์—ฌ ์žฅ๋ฒฝ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์‹ ๊ทœ ์˜คํ”ˆ ํ›„ 3๊ฐœ์›”, ๋˜๋Š” ํ‰์  ์ƒ์Šน ํ•„์š” ์‹œ + +### 1-3. ๊ฐ€์กฑ ๋‹จ์œ„ ๋ฐฉ๋ฌธ ์Šคํƒฌํ”„ ์ ๋ฆฝ + +- **์—…์ข…๋ช…**: ํ•œ์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๊ฐ€์กฑ ์‹์‚ฌ 10ํšŒ ์ ๋ฆฝ ์‹œ ๋ฌด๋ฃŒ ์‹์‚ฌ๊ถŒ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์Šคํƒฌํ”„/ํฌ์ธํŠธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 10ํšŒ ๋ฐฉ๋ฌธ ์ ๋ฆฝ ์‹œ 3๋งŒ์› ์ƒ๋‹น ์‹์‚ฌ๊ถŒ ์ œ๊ณต +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์นด์นด์˜คํ†ก ์ฑ„๋„ ๊ฐ€์ž… โ†’ ๋ฐฉ๋ฌธ ์‹œ ๋””์ง€ํ„ธ ์Šคํƒฌํ”„ ์ ๋ฆฝ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 250๋ช… (์žฌ๋ฐฉ๋ฌธ ํฌํ•จ) +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,500,000์› +- **์˜ˆ์ƒ ROI**: 380% +- **์„ฑ๊ณต ์š”์ธ**: + - ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ (์™„๋ฃŒ์œจ 40-60%) + - ๊ณ ๊ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ (์นด์นด์˜คํ†ก ์ฑ„๋„) + - ๊ฐ€์กฑ ๋‹จ์œ„ ํƒ€๊ฒŸํŒ… +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘, ํŠนํžˆ ๋ช…์ ˆ ์ „ํ›„ (์„ค๋‚ , ์ถ”์„) + +### 1-4. ์„ค๋‚  ํŠน๋ณ„ ํ•œ์ • ๋ฉ”๋‰ด SNS ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ํ•œ์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ „ํ†ต ๋–ก๊ตญ ์ธ์Šคํƒ€๊ทธ๋žจ ์ธ์ฆ ์ด๋ฒคํŠธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: SNS ๋ฐ”์ด๋Ÿด +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ธ์Šคํƒ€๊ทธ๋žจ ๊ฒŒ์‹œ๋ฌผ ์—…๋กœ๋“œ ์‹œ ์ „ํ†ต ๋–ก๊ตญ 15% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ โ†’ ์Œ์‹ ์‚ฌ์ง„ ์ดฌ์˜ โ†’ ์ธ์Šคํƒ€๊ทธ๋žจ ํ•ด์‹œํƒœ๊ทธ #OOํ•œ์‹๋‹น์„ค๋‚ ๋–ก๊ตญ + ๋งค์žฅ ํƒœ๊ทธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 200๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,800,000์› +- **์˜ˆ์ƒ ROI**: 320% +- **์„ฑ๊ณต ์š”์ธ**: + - ๊ณ„์ ˆ์„ฑ ํ™œ์šฉ (์„ค๋‚  ์ˆ˜์š”) + - ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ… (ํŒ”๋กœ์›Œ ํ™•์‚ฐ) + - ํ•œ์ • ๋ฉ”๋‰ด ํฌ์†Œ์„ฑ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ๊ฒจ์šธ (์„ค๋‚  2์ฃผ ์ „๋ถ€ํ„ฐ ๋‹น์ผ๊นŒ์ง€) + +### 1-5. VIP ๊ณ ๊ฐ ์ดˆ๋Œ€ ์‹œ์‹ํšŒ + +- **์—…์ข…๋ช…**: ํ•œ์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๋‹จ๊ณจ ๊ณ ๊ฐ๋‹˜์„ ์œ„ํ•œ ์‹ ๋ฉ”๋‰ด ์‹œ์‹ํšŒ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฐฝ์˜์  ์ด๋ฒคํŠธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ƒ์œ„ 30๋ช… VIP ๊ณ ๊ฐ ๋ฌด๋ฃŒ ์ดˆ๋Œ€, ์‹ ๋ฉ”๋‰ด ์‹œ์‹ ๋ฐ 20% ํ• ์ธ๊ถŒ ์ œ๊ณต +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์›” 3ํšŒ ์ด์ƒ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๋Œ€์ƒ ์นด์นด์˜คํ†ก ๊ฐœ๋ณ„ ์ดˆ๋Œ€ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ๊ณ ๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 30๋ช… (VIP ๋Œ€์ƒ) +- **์˜ˆ์ƒ ๋น„์šฉ**: 5,500,000์› +- **์˜ˆ์ƒ ROI**: 240% +- **์„ฑ๊ณต ์š”์ธ**: + - ๊ณ ๊ฐ ์ถฉ์„ฑ๋„ ๊ฐ•ํ™” (VIP ํ”„๋กœ๊ทธ๋žจ) + - ์ž…์†Œ๋ฌธ ๋งˆ์ผ€ํŒ… (๊ณ ๊ฐ€์น˜ ๊ณ ๊ฐ ์ „๋„์‚ฌํ™”) + - ์‹ ๋ฉ”๋‰ด ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์‹ ๋ฉ”๋‰ด ์ถœ์‹œ 1๊ฐœ์›” ์ „ + +--- + +## 2. ์น˜ํ‚จ์ง‘ (Chicken Restaurant) + +### 2-1. ํ‰์ผ ์˜คํ›„ ํƒ€์ž„ ํŠน๊ฐ€ + +- **์—…์ข…๋ช…**: ์น˜ํ‚จ์ง‘ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "ํ‰์ผ ์˜คํ›„ 2์‹œ-5์‹œ ์น˜ํ‚จ 30% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์‹œ๊ฐ„๋Œ€๋ณ„ ํ• ์ธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ํ‰์ผ ์˜คํ›„ ํŠน์ • ์‹œ๊ฐ„๋Œ€ ํ›„๋ผ์ด๋“œ/์–‘๋… ์น˜ํ‚จ 30% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€ ์ฃผ๋ฌธ ์‹œ ์ž๋™ ์ ์šฉ (๋ฐฐ๋‹ฌ/ํฌ์žฅ ๋ชจ๋‘ ๊ฐ€๋Šฅ) +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 150๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 300,000์› +- **์˜ˆ์ƒ ROI**: 420% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋น„์ˆ˜๊ธฐ ์‹œ๊ฐ„๋Œ€ ํ™œ์„ฑํ™” + - ์ฃผ๋ฐฉ ์šฉ๋Ÿ‰ ํ™œ์šฉ ๊ทน๋Œ€ํ™” + - ๋ฐฐ๋‹ฌ์•ฑ ๋ฆฌ๋ทฐ ์ฆ๊ฐ€ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํŠนํžˆ ๋ด„/๊ฐ€์„) + +### 2-2. ์Šคํฌ๋ž˜์น˜ ๋ณต๊ถŒ ์ฆ‰์„ ๋‹น์ฒจ + +- **์—…์ข…๋ช…**: ์น˜ํ‚จ์ง‘ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ฃผ๋ฌธ ์‹œ ์Šคํฌ๋ž˜์น˜ ๋ณต๊ถŒ ์ฆ์ • ์ด๋ฒคํŠธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ๊ฒฝํ’ˆ ์ด๋ฒคํŠธ (์ฆ‰์„ ๋‹น์ฒจ) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์†Œ์ฃผ/์ฝœ๋ผ/์น˜ํ‚จ๋ฌด/ํ”„๋ Œ์น˜ํ”„๋ผ์ด/์œ™๋ด‰ ์ถ”๊ฐ€/์น˜ํ‚จ 1๋งˆ๋ฆฌ (200๋ช… ๊ธฐ์ค€) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: 2๋งŒ์› ์ด์ƒ ์ฃผ๋ฌธ ์‹œ ์Šคํฌ๋ž˜์น˜ ๋ณต๊ถŒ 1์žฅ ์ฆ์ • โ†’ ์ฆ‰์‹œ ๊ธ์–ด์„œ ๋‹น์ฒจ ํ™•์ธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 200๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 485,000์› (๊ฒฝํ’ˆ ์›๊ฐ€) +- **์˜ˆ์ƒ ROI**: 450% +- **์„ฑ๊ณต ์š”์ธ**: + - ์ฆ‰๊ฐ์  ๋ณด์ƒ (๊ฒŒ์ด๋ฏธํ”ผ์ผ€์ด์…˜ +180-350% ์ฐธ์—ฌ) + - ๋ถ„์‹ค ์œ„ํ—˜ ์—†์Œ (์ฆ‰์‹œ ์‚ฌ์šฉ) + - ์žฌ๋ฏธ ์š”์†Œ (๊ณ ๊ฐ ํฅ๋ฏธ ์œ ๋ฐœ) +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘, ํŠนํžˆ ์›”๋“œ์ปต/์˜ฌ๋ฆผํ”ฝ ๋“ฑ ์Šคํฌ์ธ  ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„ + +### 2-3. ๋ฐฐ๋‹ฌ์•ฑ ๋ฆฌ๋ทฐ ์ž‘์„ฑ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ์น˜ํ‚จ์ง‘ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๋ฐฐ๋ฏผ ๋ฆฌ๋ทฐ ์ž‘์„ฑํ•˜๊ณ  ๋‹ค์Œ ์ฃผ๋ฌธ 15% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ๋ฆฌ๋ทฐ + ์ฟ ํฐ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๋ฐฐ๋‹ฌ์˜๋ฏผ์กฑ ๋ฆฌ๋ทฐ ์ž‘์„ฑ ์‹œ ๋‹ค์Œ ์ฃผ๋ฌธ 15% ํ• ์ธ ์ฟ ํฐ (1๊ฐœ์›” ์œ ํšจ) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ฃผ๋ฌธ โ†’ ๋ฐฐ๋‹ฌ ์™„๋ฃŒ ํ›„ ๋ฆฌ๋ทฐ ์ž‘์„ฑ โ†’ ์ž๋™์œผ๋กœ ์ฟ ํฐ ๋ฐœ๊ธ‰ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 220๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,650,000์› +- **์˜ˆ์ƒ ROI**: 400% +- **์„ฑ๊ณต ์š”์ธ**: + - ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ (์ฟ ํฐ ์œ ํšจ๊ธฐ๊ฐ„ 1๊ฐœ์›”) + - ๋ฐฐ๋‹ฌ์•ฑ ์ˆœ์œ„ ์ƒ์Šน (๋ฆฌ๋ทฐ ๊ฐœ์ˆ˜ โ†‘) + - ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์ž… ์ฆ๊ฐ€ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 2-4. ๋งค์ถœ ์ƒ์ƒ ๋งž์ถคํ˜• ๋งˆ์ผ€ํŒ… + +- **์—…์ข…๋ช…**: ์น˜ํ‚จ์ง‘ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "AI ๋ฐ์ดํ„ฐ ๋ถ„์„ ๊ธฐ๋ฐ˜ ๊ฐœ๋ณ„ ํ”„๋กœ๋ชจ์…˜" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฐฝ์˜์  ์ด๋ฒคํŠธ (๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๊ณ ๊ฐ๋ณ„ ๊ตฌ๋งค ํŒจํ„ด ๋ถ„์„ ํ›„ ๋งž์ถคํ˜• ํ• ์ธ/์ฟ ํฐ ์ œ๊ณต +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ž๋™ ๋ถ„์„ โ†’ ์นด์นด์˜คํ†ก/๋ฌธ์ž๋กœ ๊ฐœ๋ณ„ ํ”„๋กœ๋ชจ์…˜ ๋ฐœ์†ก +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 280๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,700,000์› +- **์˜ˆ์ƒ ROI**: 385% +- **์„ฑ๊ณต ์š”์ธ**: + - ๊ฐœ์ธํ™” ๋งˆ์ผ€ํŒ… (๊ตฌ๋งค ํŒจํ„ด ๋ฐ˜์˜) + - ์ดํƒˆ ๊ณ ๊ฐ ์žฌํ™œ์„ฑํ™” + - ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์˜์‚ฌ๊ฒฐ์ • (์ž๋‹ด์น˜ํ‚จ ์‚ฌ๋ก€: +28.4%) +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (์›” 1ํšŒ ๋ถ„์„ ๋ฐ ๋ฐœ์†ก) + +### 2-5. ์ธํ”Œ๋ฃจ์–ธ์„œ ํ˜‘์—… ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ์น˜ํ‚จ์ง‘ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์œ ๋ช… ๋จน๋ฐฉ ์œ ํŠœ๋ฒ„์™€ ํ•จ๊ป˜ํ•˜๋Š” ์‹ ๋ฉ”๋‰ด ์ถœ์‹œ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: SNS ๋ฐ”์ด๋Ÿด (์ธํ”Œ๋ฃจ์–ธ์„œ ํ˜‘์—…) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์œ ํŠœ๋ฒ„ ๋ฐฉ๋ฌธ ์˜์ƒ ๊ณต๊ฐœ + ์‹ ๋ฉ”๋‰ด 20% ํ• ์ธ (1์ฃผ์ผ ํ•œ์ •) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์œ ํŠœ๋ธŒ ์˜์ƒ ์‹œ์ฒญ โ†’ ๋งค์žฅ ๋ฐฉ๋ฌธ ๋˜๋Š” ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ ์‹œ "์œ ํŠœ๋ธŒ ๋ดค์–ด์š”" ๋ฉ˜ํŠธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ๊ณ ๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 400๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 5,800,000์› (์ธํ”Œ๋ฃจ์–ธ์„œ ๋น„์šฉ 500๋งŒ + ํ• ์ธ ๋น„์šฉ) +- **์˜ˆ์ƒ ROI**: 280% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋Œ€๊ทœ๋ชจ ๋…ธ์ถœ (์œ ํŠœ๋ฒ„ ๊ตฌ๋…์ž ์ˆ˜๋งŒ-์ˆ˜์‹ญ๋งŒ) + - ์‹ ๋ขฐ๋„ ๋†’์€ ์ถ”์ฒœ (๋จน๋ฐฉ ์œ ํŠœ๋ฒ„ ์˜ํ–ฅ๋ ฅ) + - ์‹ ๋ฉ”๋‰ด ๋น ๋ฅธ ์ธ์ง€๋„ ํ™•๋ณด +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์‹ ๋ฉ”๋‰ด ์ถœ์‹œ ์‹œ์  + +--- + +## 3. ์นดํŽ˜ (Cafe) + +### 3-1. ๋ชจ๋‹ ์ปคํ”ผ ์„ธํŠธ ํ• ์ธ + +- **์—…์ข…๋ช…**: ์นดํŽ˜ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์˜ค์ „ 7์‹œ-10์‹œ ๋ชจ๋‹ ์ปคํ”ผ+๋นต ์„ธํŠธ 3,500์›" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์‹œ๊ฐ„๋Œ€๋ณ„ ๋ฒˆ๋“ค ํ”„๋กœ๋ชจ์…˜ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์•„๋ฉ”๋ฆฌ์นด๋…ธ + ํฌ๋ฃจ์•„์ƒ ์„ธํŠธ 3,500์› (์ •์ƒ๊ฐ€ 6,500์›) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€ ๋งค์žฅ ๋ฐฉ๋ฌธ ๋˜๋Š” ํ…Œ์ดํฌ์•„์›ƒ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 200๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 250,000์› +- **์˜ˆ์ƒ ROI**: 480% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋น„์ˆ˜๊ธฐ ์‹œ๊ฐ„๋Œ€ ํ™œ์„ฑํ™” (์•„์นจ ์‹œ๊ฐ„) + - ์ง์žฅ์ธ ํƒ€๊ฒŸํŒ… + - ์„ธํŠธ ๊ตฌ์„ฑ์œผ๋กœ ๊ฐ๋‹จ๊ฐ€ ์ƒ์Šน +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํ‰์ผ ์ค‘์‹ฌ) + +### 3-2. ๋””์ง€ํ„ธ ์Šคํƒฌํ”„ 10+1 ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ์นดํŽ˜ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ปคํ”ผ 10์ž” ๋งˆ์‹œ๋ฉด 1์ž” ๋ฌด๋ฃŒ!" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์Šคํƒฌํ”„/ํฌ์ธํŠธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 10ํšŒ ๊ตฌ๋งค ์ ๋ฆฝ ์‹œ ์•„๋ฉ”๋ฆฌ์นด๋…ธ 1์ž” ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์นด์นด์˜คํ†ก ์ฑ„๋„ ๋˜๋Š” ์ž์ฒด ์•ฑ ๊ฐ€์ž… โ†’ ๊ตฌ๋งค ์‹œ ์ž๋™ ์ ๋ฆฝ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 180๋ช… (์žฌ๋ฐฉ๋ฌธ ํฌํ•จ ์•ฝ 2,000ํšŒ) +- **์˜ˆ์ƒ ๋น„์šฉ**: 270,000์› +- **์˜ˆ์ƒ ROI**: 520% +- **์„ฑ๊ณต ์š”์ธ**: + - ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ (์™„๋ฃŒ์œจ 40-60%) + - ๊ณ ๊ฐ ์ถฉ์„ฑ๋„ ๊ตฌ์ถ• + - ์ข…์ด ์Šคํƒฌํ”„ ๋ถ„์‹ค ๋ฐฉ์ง€ (๋””์ง€ํ„ธ) +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 3-3. ์ธ์Šคํƒ€๊ทธ๋žจ ๊ฐ์„ฑ ํฌํ† ์กด ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ์นดํŽ˜ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "ํฌํ† ์กด์—์„œ ์ธ์ฆ์ƒท ์˜ฌ๋ฆฌ๊ณ  ์Œ๋ฃŒ 20% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: SNS ๋ฐ”์ด๋Ÿด +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ธ์Šคํƒ€๊ทธ๋žจ ํฌํ† ์กด ์‚ฌ์ง„ ์—…๋กœ๋“œ + ๋งค์žฅ ํƒœ๊ทธ ์‹œ 20% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ โ†’ ํฌํ† ์กด ์ดฌ์˜ โ†’ ์ธ์Šคํƒ€๊ทธ๋žจ ์—…๋กœ๋“œ + #OO์นดํŽ˜ ํ•ด์‹œํƒœ๊ทธ โ†’ ์ง์›์—๊ฒŒ ํ™”๋ฉด ์ œ์‹œ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ (ํฌํ† ์กด ์ œ์ž‘ ํฌํ•จ) +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 250๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,800,000์› (ํฌํ† ์กด ์ œ์ž‘ 100๋งŒ + ํ• ์ธ ๋น„์šฉ) +- **์˜ˆ์ƒ ROI**: 340% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ… (์ธ์Šคํƒ€๊ทธ๋ž˜๋จธ๋ธ” ๊ณต๊ฐ„) + - 2030 ์—ฌ์„ฑ ํƒ€๊ฒŸํŒ… (์„ฑ์ˆ˜๋™ ์นดํŽ˜ ์‚ฌ๋ก€) + - UGC ์ž๋ฐœ์  ํ™•์‚ฐ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํŠนํžˆ ๋ด„/๊ฐ€์„ ๊ฐ์„ฑ ์‹œ์ฆŒ) + +### 3-4. ์ƒ์ผ ๊ณ ๊ฐ ํŠน๋ณ„ ํ˜œํƒ + +- **์—…์ข…๋ช…**: ์นดํŽ˜ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ƒ์ผ ๊ณ ๊ฐ๋‹˜๊ป˜ ์Œ๋ฃŒ 1์ž” ๋ฌด๋ฃŒ ์„ ๋ฌผ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ (๊ณ ๊ฐ ์„ธ๊ทธ๋จผํŠธ) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ƒ์ผ ๋‹น์›” ๋ฐฉ๋ฌธ ์‹œ ์Œ๋ฃŒ 1์ž” ๋ฌด๋ฃŒ (5์ฒœ์› ์ดํ•˜ ๋ฉ”๋‰ด) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์นด์นด์˜คํ†ก ์ฑ„๋„ ๊ฐ€์ž… ์‹œ ์ƒ์ผ ๋“ฑ๋ก โ†’ ์ƒ์ผ ๋‹ฌ ์ฟ ํฐ ์ž๋™ ๋ฐœ์†ก +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 200๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,500,000์› +- **์˜ˆ์ƒ ROI**: 360% +- **์„ฑ๊ณต ์š”์ธ**: + - ๊ฐœ์ธํ™” ๋งˆ์ผ€ํŒ… (ํŠน๋ณ„ํ•œ ๋‚  ํ˜œํƒ) + - ๋†’์€ ์žฌ๋ฐฉ๋ฌธ์œจ (์ƒ์ผ ๊ณ ๊ฐ ROI ๋†’์Œ) + - ๊ณ ๊ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 3-5. ๋ ˆํŠธ๋กœ ์ฝœ๋ผ๋ณด ํ•œ์ • ๋ฉ”๋‰ด + +- **์—…์ข…๋ช…**: ์นดํŽ˜ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๊ฐ์„ฑ์ปคํ”ผ X ๋ฐ”๋‚˜๋‚˜ํ‚ฅ ํ•œ์ • ์ฝœ๋ผ๋ณด ๋ฉ”๋‰ด" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฐฝ์˜์  ์ด๋ฒคํŠธ (๋ธŒ๋žœ๋“œ ํ˜‘์—…) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๋ฐ”๋‚˜๋‚˜ํ‚ฅ ๋ผ๋–ผ + ์นด๋ผ๋ฉœ ์‰์ดํฌ ํ•œ์ • ํŒ๋งค (2์ฃผ๊ฐ„) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ๋˜๋Š” ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ๊ณ ๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 350๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 5,200,000์› (ํ˜‘์—… ๋น„์šฉ + ์žฌ๋ฃŒ๋น„ + ๋งˆ์ผ€ํŒ…) +- **์˜ˆ์ƒ ROI**: 290% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋ ˆํŠธ๋กœ ํ–ฅ์ˆ˜ ์ž๊ทน (MZ์„ธ๋Œ€ ํƒ€๊ฒŸ) + - ๋ฐ”์ด๋Ÿด ํ™”์ œ์„ฑ (๊ฐ์„ฑ์ปคํ”ผ ์‚ฌ๋ก€: ์ตœ์†Œ ๋น„์šฉ์œผ๋กœ ์„ฑ๊ณต) + - ํ•œ์ •ํŒ ํฌ์†Œ์„ฑ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํŠธ๋ Œ๋“œ ๋งž์ถคํ˜•) + +--- + +## 4. ์ค‘์‹๋‹น (Chinese Restaurant) + +### 4-1. ๋Ÿฐ์น˜ ์„ธํŠธ ํŠน๊ฐ€ + +- **์—…์ข…๋ช…**: ์ค‘์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ ์‹ฌ ์‹œ๊ฐ„ ์งœ์žฅ๋ฉด+ํƒ•์ˆ˜์œก ์„ธํŠธ 9,900์›" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ๋ฒˆ๋“ค ํ”„๋กœ๋ชจ์…˜ (์‹œ๊ฐ„๋Œ€๋ณ„) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 11์‹œ-14์‹œ ์งœ์žฅ๋ฉด+์†Œํƒ•์ˆ˜์œก ์„ธํŠธ 9,900์› (์ •์ƒ๊ฐ€ 14,000์›) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€ ๋ฐฉ๋ฌธ ๋˜๋Š” ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 180๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 280,000์› +- **์˜ˆ์ƒ ROI**: 380% +- **์„ฑ๊ณต ์š”์ธ**: + - ์ง์žฅ์ธ ์ ์‹ฌ ์ˆ˜์š” ํƒ€๊ฒŸ + - ์„ธํŠธ ๋ฉ”๋‰ด๋กœ ๊ฐ๋‹จ๊ฐ€ ์œ ์ง€ + - ๋ฐฐ๋‹ฌ ์‹œ๊ฐ„๋Œ€ ์ง‘์ค‘ ํ™œ์šฉ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํ‰์ผ) + +### 4-2. ์ฒซ ์ฃผ๋ฌธ ๊ณ ๊ฐ ์ฟ ํฐ + +- **์—…์ข…๋ช…**: ์ค‘์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์‹ ๊ทœ ๊ณ ๊ฐ ํ™˜์˜! ์ฒซ ์ฃผ๋ฌธ 20% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฟ ํฐ (์‹ ๊ทœ ๊ณ ๊ฐ ํƒ€๊ฒŸ) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ฒซ ์ฃผ๋ฌธ ์‹œ ์ „ ๋ฉ”๋‰ด 20% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋ฐฐ๋‹ฌ์•ฑ ์‹ ๊ทœ ์ฃผ๋ฌธ ๊ณ ๊ฐ ์ž๋™ ์ ์šฉ ๋˜๋Š” ๋งค์žฅ ๋ฐฉ๋ฌธ ์‹œ ์ฒซ ๋ฐฉ๋ฌธ ๋ฉ˜ํŠธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 150๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 300,000์› +- **์˜ˆ์ƒ ROI**: 340% +- **์„ฑ๊ณต ์š”์ธ**: + - ์‹ ๊ทœ ๊ณ ๊ฐ ํ™•๋ณด + - ๋†’์€ ์žฌ๋ฐฉ๋ฌธ ์ „ํ™˜์œจ (30-40%) + - ์ง„์ž… ์žฅ๋ฒฝ ๋‚ฎ์ถค +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (์‹ ๊ทœ ์˜คํ”ˆ ๋˜๋Š” ์‹ ๊ทœ ๊ณ ๊ฐ ํ™•๋ณด ํ•„์š” ์‹œ) + +### 4-3. ๋ฐฐ๋‹ฌ์•ฑ ๋‹จ๊ณจ ๊ณ ๊ฐ ๋ฆฌ์›Œ๋“œ + +- **์—…์ข…๋ช…**: ์ค‘์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ด๋‹ฌ์˜ ๋‹จ๊ณจ ๊ณ ๊ฐ 5ํšŒ ์ฃผ๋ฌธ ์‹œ ํƒ•์ˆ˜์œก ๋ฌด๋ฃŒ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์Šคํƒฌํ”„/ํฌ์ธํŠธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์›” 5ํšŒ ์ฃผ๋ฌธ ๋‹ฌ์„ฑ ์‹œ ์†Œํƒ•์ˆ˜์œก 1์ ‘์‹œ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋ฐฐ๋‹ฌ์•ฑ ์ฃผ๋ฌธ ํšŸ์ˆ˜ ์ž๋™ ์ง‘๊ณ„ โ†’ 5ํšŒ ๋‹ฌ์„ฑ ์‹œ ์ฟ ํฐ ๋ฐœ๊ธ‰ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 200๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,600,000์› +- **์˜ˆ์ƒ ROI**: 400% +- **์„ฑ๊ณต ์š”์ธ**: + - ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ (์›” 5ํšŒ ๋ชฉํ‘œ) + - ๊ณ ๊ฐ ์ถฉ์„ฑ๋„ ๊ฐ•ํ™” + - ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ ๋น„์ค‘ ์ฆ๊ฐ€ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 4-4. ๊ฐ€์กฑ ๋‹จ์œ„ ๋ฐฉ๋ฌธ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ์ค‘์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "4์ธ ์ด์ƒ ๊ฐ€์กฑ ๋ฐฉ๋ฌธ ์‹œ ์Œ๋ฃŒ ๋ฌด๋ฃŒ + ์งฌ๋ฝ• 1๊ฐœ ์„œ๋น„์Šค" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ๋ฒˆ๋“ค ํ”„๋กœ๋ชจ์…˜ (๊ทธ๋ฃน ํƒ€๊ฒŸ) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 4์ธ ์ด์ƒ ๊ฐ€์กฑ ๋ฐฉ๋ฌธ ์‹œ ์Œ๋ฃŒ 4์ž” + ์งฌ๋ฝ• 1๊ทธ๋ฆ‡ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ ์‹œ 4์ธ ์ด์ƒ ํ…Œ์ด๋ธ” ์˜ˆ์•ฝ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 180๋ช… (์•ฝ 45๊ทธ๋ฃน) +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,750,000์› +- **์˜ˆ์ƒ ROI**: 325% +- **์„ฑ๊ณต ์š”์ธ**: + - ๊ฐ€์กฑ ์™ธ์‹ ์ˆ˜์š” ํƒ€๊ฒŸ (์ฃผ๋ง/๊ณตํœด์ผ) + - ๊ฐ๋‹จ๊ฐ€ ์ƒ์Šน (4์ธ ์ด์ƒ ์ฃผ๋ฌธ) + - ๋งค์žฅ ๋‚ด ์‹์‚ฌ ํ™œ์„ฑํ™” +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์ฃผ๋ง/๊ณตํœด์ผ, ํŠนํžˆ ์–ด๋ฆฐ์ด๋‚ /๊ฐ€์กฑ์˜ ๋‹ฌ(5์›”) + +### 4-5. ๋ช…์ ˆ ํŠน๋ณ„ ์„ธํŠธ ์˜ˆ์•ฝ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ์ค‘์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์„ค๋‚  ํŠน์„  ์ฝ”์Šค ์˜ˆ์•ฝ ๊ณ ๊ฐ 30% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฐฝ์˜์  ์ด๋ฒคํŠธ (๊ณ„์ ˆ ํ•œ์ •) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์„ค๋‚  ์ „ํ›„ 1์ฃผ์ผ ํŠน์„  ์ฝ”์Šค ์˜ˆ์•ฝ ์‹œ 30% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ „ํ™” ๋˜๋Š” ์นด์นด์˜คํ†ก ์˜ˆ์•ฝ (3์ผ ์ „๊นŒ์ง€) +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ๊ณ ๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 280๋ช… (์•ฝ 70๊ทธ๋ฃน) +- **์˜ˆ์ƒ ๋น„์šฉ**: 6,000,000์› +- **์˜ˆ์ƒ ROI**: 250% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋ช…์ ˆ ์ˆ˜์š” ํ™œ์šฉ (๊ฐ€์กฑ ์™ธ์‹) + - ์‚ฌ์ „ ์˜ˆ์•ฝ์œผ๋กœ ์žฌ๊ณ  ๊ด€๋ฆฌ ์šฉ์ด + - ๊ณ ๊ฐ ํ™•๋ณด (๋ช…์ ˆ ํ›„ ์žฌ๋ฐฉ๋ฌธ ์œ ๋„) +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ๊ฒจ์šธ (์„ค๋‚  2์ฃผ ์ „๋ถ€ํ„ฐ) + +--- + +## 5. ์ผ์‹๋‹น (Japanese Restaurant) + +### 5-1. ํ‰์ผ ์ดˆ๋ฐฅ ์„ธํŠธ ํ• ์ธ + +- **์—…์ข…๋ช…**: ์ผ์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "ํ‰์ผ ๋Ÿฐ์น˜ ์ดˆ๋ฐฅ ์„ธํŠธ 25% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์‹œ๊ฐ„๋Œ€๋ณ„ ํ• ์ธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์›”-๊ธˆ 11์‹œ-15์‹œ ํŠน์ • ์ดˆ๋ฐฅ ์„ธํŠธ 25% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€ ๋งค์žฅ ๋ฐฉ๋ฌธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 160๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 290,000์› +- **์˜ˆ์ƒ ROI**: 360% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋น„์ˆ˜๊ธฐ ์‹œ๊ฐ„๋Œ€ ํ™œ์šฉ + - ์ง์žฅ์ธ ์ ์‹ฌ ํƒ€๊ฒŸ + - ๊ณ ๊ธ‰ ๋ฉ”๋‰ด ์ ‘๊ทผ์„ฑ ํ–ฅ์ƒ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํ‰์ผ) + +### 5-2. ์ธ์Šคํƒ€๊ทธ๋žจ ์‚ฌ์ผ€ ๋ฆฌ๋ทฐ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ์ผ์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์‚ฌ์ผ€์™€ ์•ˆ์ฃผ ์ธ์Šคํƒ€๊ทธ๋žจ ์ธ์ฆ ์‹œ 10% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: SNS ๋ฐ”์ด๋Ÿด +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ธ์Šคํƒ€๊ทธ๋žจ ์‚ฌ์ง„ ์—…๋กœ๋“œ + ํ•ด์‹œํƒœ๊ทธ ์‹œ 10% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ โ†’ ์Œ์‹ ์‚ฌ์ง„ ์ดฌ์˜ โ†’ ์ธ์Šคํƒ€๊ทธ๋žจ ์—…๋กœ๋“œ + #OO์ผ์‹๋‹น์‚ฌ์ผ€ โ†’ ์ง์› ํ™•์ธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 140๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 270,000์› +- **์˜ˆ์ƒ ROI**: 310% +- **์„ฑ๊ณต ์š”์ธ**: + - 3040์„ธ๋Œ€ ํƒ€๊ฒŸ (์ธ์Šคํƒ€๊ทธ๋žจ ์‚ฌ์šฉ์ธต) + - ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ… + - ํ”„๋ฆฌ๋ฏธ์—„ ์ด๋ฏธ์ง€ ๊ฐ•ํ™” +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 5-3. ์Šคํƒฌํ”„ ์ ๋ฆฝ ์˜ค๋งˆ์นด์„ธ ํ˜œํƒ + +- **์—…์ข…๋ช…**: ์ผ์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "10ํšŒ ๋ฐฉ๋ฌธ ์‹œ ์˜ค๋งˆ์นด์„ธ 20% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์Šคํƒฌํ”„/ํฌ์ธํŠธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 10ํšŒ ๋ฐฉ๋ฌธ ์ ๋ฆฝ ์‹œ ์˜ค๋งˆ์นด์„ธ ์ฝ”์Šค 20% ํ• ์ธ๊ถŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์นด์นด์˜คํ†ก ์ฑ„๋„ ๊ฐ€์ž… โ†’ ๋ฐฉ๋ฌธ ์‹œ ์Šคํƒฌํ”„ ์ ๋ฆฝ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 220๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,650,000์› +- **์˜ˆ์ƒ ROI**: 370% +- **์„ฑ๊ณต ์š”์ธ**: + - ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ (10ํšŒ ๋ชฉํ‘œ) + - ๊ณ ๊ฐ€ ๋ฉ”๋‰ด ์—…์…€๋ง + - ๊ณ ๊ฐ ์ถฉ์„ฑ๋„ ๊ตฌ์ถ• +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 5-4. ๊ธฐ๋…์ผ ๊ณ ๊ฐ ํŠน๋ณ„ ์„œ๋น„์Šค + +- **์—…์ข…๋ช…**: ์ผ์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ƒ์ผ/๊ธฐ๋…์ผ ๊ณ ๊ฐ ๋””์ €ํŠธ + ์‚ฌ์ง„ ์„œ๋น„์Šค" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ (๊ณ ๊ฐ ์„ธ๊ทธ๋จผํŠธ) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ƒ์ผ/๊ธฐ๋…์ผ ๋ฐฉ๋ฌธ ์‹œ ๋””์ €ํŠธ ๋ฌด๋ฃŒ + ํด๋ผ๋กœ์ด๋“œ ์‚ฌ์ง„ ์ฆ์ • +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์˜ˆ์•ฝ ์‹œ ๊ธฐ๋…์ผ ๋ฉ˜ํŠธ โ†’ ๋‹น์ผ ์ง์› ์•ˆ๋‚ด +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 180๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,700,000์› +- **์˜ˆ์ƒ ROI**: 340% +- **์„ฑ๊ณต ์š”์ธ**: + - ํŠน๋ณ„ํ•œ ๋‚  ๊ฒฝํ—˜ ์ œ๊ณต + - ๊ณ ๊ฐ ๊ฐ๋™ (๊ฐ์„ฑ ๋งˆ์ผ€ํŒ…) + - ์žฌ๋ฐฉ๋ฌธ + ์ž…์†Œ๋ฌธ ํšจ๊ณผ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 5-5. ๊ณ„์ ˆ ํ•œ์ • ์˜ค๋งˆ์นด์„ธ VIP ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ์ผ์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๋ด„ ์ œ์ฒ  ์˜ค๋งˆ์นด์„ธ ํŠน๋ณ„ ์ฝ”์Šค ์ดˆ๋Œ€" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฐฝ์˜์  ์ด๋ฒคํŠธ (๊ณ„์ ˆ ํ•œ์ •) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ƒ์œ„ 20๋ช… VIP ๊ณ ๊ฐ ๋ด„ ์ œ์ฒ  ์˜ค๋งˆ์นด์„ธ ํŠน๋ณ„ ์ฝ”์Šค ์ดˆ๋Œ€ (15% ํ• ์ธ) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์›” 2ํšŒ ์ด์ƒ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๋Œ€์ƒ ๊ฐœ๋ณ„ ์ดˆ๋Œ€์žฅ ๋ฐœ์†ก +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ๊ณ ๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 20๋ช… (VIP ๋Œ€์ƒ) +- **์˜ˆ์ƒ ๋น„์šฉ**: 5,500,000์› +- **์˜ˆ์ƒ ROI**: 260% +- **์„ฑ๊ณต ์š”์ธ**: + - VIP ๊ณ ๊ฐ ์ถฉ์„ฑ๋„ ๊ฐ•ํ™” + - ๊ณ„์ ˆ์„ฑ ํ™œ์šฉ (๋ด„ ์ œ์ฒ  ์‹์žฌ๋ฃŒ) + - ๊ณ ๊ฐ€ ๋ฉ”๋‰ด ํ”„๋กœ๋ชจ์…˜ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ๋ด„ (3-5์›”) + +--- + +## 6. ์–‘์‹๋‹น (Italian Restaurant) + +### 6-1. ๋Ÿฐ์น˜ ํŒŒ์Šคํƒ€ ์„ธํŠธ ํ• ์ธ + +- **์—…์ข…๋ช…**: ์–‘์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "ํ‰์ผ ์ ์‹ฌ ํŒŒ์Šคํƒ€+์ƒ๋Ÿฌ๋“œ+์Œ๋ฃŒ ์„ธํŠธ 12,900์›" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ๋ฒˆ๋“ค ํ”„๋กœ๋ชจ์…˜ (์‹œ๊ฐ„๋Œ€๋ณ„) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์›”-๊ธˆ 11์‹œ-15์‹œ ํŒŒ์Šคํƒ€ ์„ธํŠธ 12,900์› (์ •์ƒ๊ฐ€ 18,000์›) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€ ๋งค์žฅ ๋ฐฉ๋ฌธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 190๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 280,000์› +- **์˜ˆ์ƒ ROI**: 400% +- **์„ฑ๊ณต ์š”์ธ**: + - ์ง์žฅ์ธ ์ ์‹ฌ ํƒ€๊ฒŸ + - ์„ธํŠธ ๊ตฌ์„ฑ์œผ๋กœ ๊ฐ€์น˜ ์ธ์‹ ํ–ฅ์ƒ + - ๋น„์ˆ˜๊ธฐ ์‹œ๊ฐ„๋Œ€ ํ™œ์„ฑํ™” +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํ‰์ผ) + +### 6-2. ์™€์ธ ํŽ˜์–ด๋ง SNS ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ์–‘์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์Œ์‹๊ณผ ์™€์ธ ํŽ˜์–ด๋ง ์ธ์Šคํƒ€๊ทธ๋žจ ์ธ์ฆ ์ด๋ฒคํŠธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: SNS ๋ฐ”์ด๋Ÿด +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ธ์Šคํƒ€๊ทธ๋žจ ํŽ˜์–ด๋ง ์‚ฌ์ง„ ์—…๋กœ๋“œ ์‹œ ์™€์ธ 1์ž” ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ โ†’ ์Œ์‹+์™€์ธ ์‚ฌ์ง„ ์ดฌ์˜ โ†’ ์ธ์Šคํƒ€๊ทธ๋žจ ์—…๋กœ๋“œ + #OO์–‘์‹๋‹น์™€์ธํŽ˜์–ด๋ง +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 130๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 260,000์› +- **์˜ˆ์ƒ ROI**: 320% +- **์„ฑ๊ณต ์š”์ธ**: + - ํ”„๋ฆฌ๋ฏธ์—„ ์ด๋ฏธ์ง€ ๊ฐ•ํ™” + - 3040์„ธ๋Œ€ ํƒ€๊ฒŸ + - UGC ํ™•์‚ฐ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 6-3. ๋””๋„ˆ ์ฝ”์Šค ์˜ˆ์•ฝ ๊ณ ๊ฐ ๋””์ €ํŠธ ์„œ๋น„์Šค + +- **์—…์ข…๋ช…**: ์–‘์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๋””๋„ˆ ์ฝ”์Šค ์˜ˆ์•ฝ ์‹œ ๋””์ €ํŠธ ๋ฌด๋ฃŒ ์—…๊ทธ๋ ˆ์ด๋“œ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 2์ผ ์ „ ์˜ˆ์•ฝ ์‹œ ๋””์ €ํŠธ 1๊ฐœ ๋ฌด๋ฃŒ (ํ‹ฐ๋ผ๋ฏธ์ˆ˜/ํŒ๋‚˜์ฝ”ํƒ€) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ „ํ™” ๋˜๋Š” ๋„ค์ด๋ฒ„ ์˜ˆ์•ฝ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 240๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,600,000์› +- **์˜ˆ์ƒ ROI**: 365% +- **์„ฑ๊ณต ์š”์ธ**: + - ์‚ฌ์ „ ์˜ˆ์•ฝ ์œ ๋„ (์žฌ๊ณ  ๊ด€๋ฆฌ) + - ๋””๋„ˆ ์‹œ๊ฐ„๋Œ€ ํ™œ์„ฑํ™” + - ๊ณ ๊ฐ ๋งŒ์กฑ๋„ ํ–ฅ์ƒ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํŠนํžˆ ์ฃผ๋ง) + +### 6-4. ์ปคํ”Œ ๊ธฐ๋…์ผ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ์–‘์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ปคํ”Œ ๊ธฐ๋…์ผ ํŠน๋ณ„ ํ…Œ์ด๋ธ” ์„ธํŒ… + ์ƒดํŽ˜์ธ ์„œ๋น„์Šค" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฐฝ์˜์  ์ด๋ฒคํŠธ (๊ณ ๊ฐ ์„ธ๊ทธ๋จผํŠธ) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๊ธฐ๋…์ผ ์˜ˆ์•ฝ ์ปคํ”Œ ๋Œ€์ƒ ํŠน๋ณ„ ํ…Œ์ด๋ธ” ์„ธํŒ… + ์ƒดํŽ˜์ธ 1๋ณ‘ + ํด๋ผ๋กœ์ด๋“œ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์˜ˆ์•ฝ ์‹œ ๊ธฐ๋…์ผ ๋ฉ˜ํŠธ โ†’ ๋‹น์ผ ํŠน๋ณ„ ์ค€๋น„ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 200๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,800,000์› +- **์˜ˆ์ƒ ROI**: 350% +- **์„ฑ๊ณต ์š”์ธ**: + - ๊ฐ์„ฑ ๋งˆ์ผ€ํŒ… (ํŠน๋ณ„ํ•œ ๊ฒฝํ—˜) + - ์ปคํ”Œ ํƒ€๊ฒŸํŒ… (๋ฐธ๋Ÿฐํƒ€์ธ/ํ™”์ดํŠธ๋ฐ์ด) + - ์ž…์†Œ๋ฌธ ํšจ๊ณผ (SNS ๊ณต์œ ) +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํŠนํžˆ 2์›”, 3์›”, ํฌ๋ฆฌ์Šค๋งˆ์Šค) + +### 6-5. ์…ฐํ”„ ํŠน์„  ์‹œ์‹ํšŒ + +- **์—…์ข…๋ช…**: ์–‘์‹๋‹น +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์›” 1ํšŒ ์…ฐํ”„ ํŠน์„  ๋ฉ”๋‰ด ์‹œ์‹ํšŒ ์ดˆ๋Œ€" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฐฝ์˜์  ์ด๋ฒคํŠธ (VIP) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์›” 1ํšŒ VIP ๊ณ ๊ฐ 30๋ช… ์ดˆ๋Œ€, ์‹ ๋ฉ”๋‰ด ์‹œ์‹ + 20% ํ• ์ธ๊ถŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์›” 2ํšŒ ์ด์ƒ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๋Œ€์ƒ ์นด์นด์˜คํ†ก ์ดˆ๋Œ€ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ๊ณ ๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 30๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 5,300,000์› +- **์˜ˆ์ƒ ROI**: 270% +- **์„ฑ๊ณต ์š”์ธ**: + - VIP ๊ณ ๊ฐ ์ถฉ์„ฑ๋„ ๊ฐ•ํ™” + - ์‹ ๋ฉ”๋‰ด ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘ + - ์ž…์†Œ๋ฌธ ๋งˆ์ผ€ํŒ… +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (์›” 1ํšŒ) + +--- + +## 7. ๋ฒ ์ด์ปค๋ฆฌ (Bakery) + +### 7-1. ์•„์นจ ๋นต ํŠน๊ฐ€ + +- **์—…์ข…๋ช…**: ๋ฒ ์ด์ปค๋ฆฌ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์˜ค์ „ 7์‹œ-10์‹œ ๊ฐ“ ๊ตฌ์šด ๋นต 30% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์‹œ๊ฐ„๋Œ€๋ณ„ ํ• ์ธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์˜ค์ „ ํŠน์ • ์‹œ๊ฐ„๋Œ€ ๋นต 30% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€ ๋งค์žฅ ๋ฐฉ๋ฌธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 210๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 270,000์› +- **์˜ˆ์ƒ ROI**: 450% +- **์„ฑ๊ณต ์š”์ธ**: + - ์•„์นจ ์‹œ๊ฐ„๋Œ€ ํ™œ์„ฑํ™” + - ์‹ ์„ ํ•จ ๊ฐ•์กฐ (๊ฐ“ ๊ตฌ์šด ๋นต) + - ์ง์žฅ์ธ ์ถœ๊ทผ๊ธธ ํƒ€๊ฒŸ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํ‰์ผ ์ค‘์‹ฌ) + +### 7-2. ์ธ์Šคํƒ€๊ทธ๋žจ ๋นต ์ธ์ฆ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ๋ฒ ์ด์ปค๋ฆฌ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์˜ˆ์œ ๋นต ์‚ฌ์ง„ ์ธ์Šคํƒ€๊ทธ๋žจ ์˜ฌ๋ฆฌ๊ณ  ์ปคํ”ผ ๋ฌด๋ฃŒ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: SNS ๋ฐ”์ด๋Ÿด +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ธ์Šคํƒ€๊ทธ๋žจ ๋นต ์‚ฌ์ง„ ์—…๋กœ๋“œ + ํ•ด์‹œํƒœ๊ทธ ์‹œ ์ปคํ”ผ 1์ž” ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ โ†’ ๋นต ์‚ฌ์ง„ ์ดฌ์˜ โ†’ ์ธ์Šคํƒ€๊ทธ๋žจ ์—…๋กœ๋“œ + #OO๋ฒ ์ด์ปค๋ฆฌ โ†’ ์ง์› ํ™•์ธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 170๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 250,000์› +- **์˜ˆ์ƒ ROI**: 380% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ… (์ธ์Šคํƒ€๊ทธ๋ž˜๋จธ๋ธ”) + - 2030 ์—ฌ์„ฑ ํƒ€๊ฒŸ + - UGC ํ™•์‚ฐ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 7-3. ์ƒ์ผ ์ผ€์ดํฌ ์˜ˆ์•ฝ ๊ณ ๊ฐ ํ• ์ธ + +- **์—…์ข…๋ช…**: ๋ฒ ์ด์ปค๋ฆฌ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ƒ์ผ ์ผ€์ดํฌ 3์ผ ์ „ ์˜ˆ์•ฝ ์‹œ 15% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฟ ํฐ (์˜ˆ์•ฝ ์œ ๋„) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 3์ผ ์ „ ์˜ˆ์•ฝ ์‹œ ์ผ€์ดํฌ 15% ํ• ์ธ + ์ดˆ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ „ํ™” ๋˜๋Š” ์นด์นด์˜คํ†ก ์˜ˆ์•ฝ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 260๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,700,000์› +- **์˜ˆ์ƒ ROI**: 370% +- **์„ฑ๊ณต ์š”์ธ**: + - ์‚ฌ์ „ ์˜ˆ์•ฝ ์œ ๋„ (์žฌ๊ณ  ๊ด€๋ฆฌ) + - ์ƒ์ผ ์ˆ˜์š” ํ™•๋ณด + - ๊ณ ๊ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 7-4. ์›”๊ฐ„ ์‹ ์ œํ’ˆ ์ถœ์‹œ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ๋ฒ ์ด์ปค๋ฆฌ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ด๋‹ฌ์˜ ์‹ ์ƒ ๋นต ์ถœ์‹œ! SNS ๊ณต์œ ํ•˜๊ณ  20% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: SNS ๋ฐ”์ด๋Ÿด + ํ• ์ธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์‹ ์ œํ’ˆ ๊ตฌ๋งค + SNS ๊ณต์œ  ์‹œ 20% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์‹ ์ œํ’ˆ ๊ตฌ๋งค โ†’ SNS ๊ณต์œ  โ†’ ์ง์› ํ™•์ธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 230๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,650,000์› +- **์˜ˆ์ƒ ROI**: 350% +- **์„ฑ๊ณต ์š”์ธ**: + - ์‹ ์ œํ’ˆ ๋น ๋ฅธ ์ธ์ง€๋„ ํ™•๋ณด + - ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ… + - ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (์›” 1ํšŒ) + +### 7-5. ์ธํ”Œ๋ฃจ์–ธ์„œ ํ˜‘์—… ํŒ์—… ์Šคํ† ์–ด + +- **์—…์ข…๋ช…**: ๋ฒ ์ด์ปค๋ฆฌ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ธ๊ธฐ ๋ฒ ์ดํ‚น ์œ ํŠœ๋ฒ„์™€ ํ•จ๊ป˜ํ•˜๋Š” ํŠน๋ณ„ ํŒ์—…" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฐฝ์˜์  ์ด๋ฒคํŠธ (์ธํ”Œ๋ฃจ์–ธ์„œ ํ˜‘์—…) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์œ ํŠœ๋ฒ„ ์ฝœ๋ผ๋ณด ๋นต ํ•œ์ • ํŒ๋งค + ์‚ฌ์ธํšŒ + ๋ฒ ์ดํ‚น ํด๋ž˜์Šค +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํŒ์—… ๊ธฐ๊ฐ„ ๋งค์žฅ ๋ฐฉ๋ฌธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ๊ณ ๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 350๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 5,800,000์› (์ธํ”Œ๋ฃจ์–ธ์„œ ๋น„์šฉ 500๋งŒ + ์žฌ๋ฃŒ๋น„ + ๋งˆ์ผ€ํŒ…) +- **์˜ˆ์ƒ ROI**: 280% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋Œ€๊ทœ๋ชจ ๋…ธ์ถœ (์œ ํŠœ๋ฒ„ ๊ตฌ๋…์ž) + - ํ•œ์ •ํŒ ํฌ์†Œ์„ฑ + - ์ฒดํ—˜ ์ด๋ฒคํŠธ (๋ฒ ์ดํ‚น ํด๋ž˜์Šค) +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ํŠน๋ณ„ ์‹œ์ฆŒ (ํฌ๋ฆฌ์Šค๋งˆ์Šค, ๋ฐธ๋Ÿฐํƒ€์ธ ๋“ฑ) + +--- + +## 8. ํŒจ์ŠคํŠธํ‘ธ๋“œ (Fast Food / Hamburger) + +### 8-1. ์ ์‹ฌ ์‹œ๊ฐ„๋Œ€ ์„ธํŠธ ํ• ์ธ + +- **์—…์ข…๋ช…**: ํŒจ์ŠคํŠธํ‘ธ๋“œ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๋Ÿฐ์น˜ ํƒ€์ž„ ๋ฒ„๊ฑฐ ์„ธํŠธ 1,000์› ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์‹œ๊ฐ„๋Œ€๋ณ„ ํ• ์ธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 11์‹œ-14์‹œ ๋ฒ„๊ฑฐ ์„ธํŠธ 1,000์› ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€ ๋งค์žฅ ๋˜๋Š” ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 220๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 280,000์› +- **์˜ˆ์ƒ ROI**: 410% +- **์„ฑ๊ณต ์š”์ธ**: + - ์ ์‹ฌ ์‹œ๊ฐ„๋Œ€ ์ง‘์ค‘ ๊ณต๋žต + - ์ง์žฅ์ธ ๋น ๋ฅธ ์‹์‚ฌ ๋‹ˆ์ฆˆ + - ๊ฐ๋‹จ๊ฐ€ ์œ ์ง€ (์„ธํŠธ ํŒ๋งค) +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํ‰์ผ) + +### 8-2. ๋ฐฐ๋‹ฌ์•ฑ ์ฒซ ์ฃผ๋ฌธ ํ• ์ธ + +- **์—…์ข…๋ช…**: ํŒจ์ŠคํŠธํ‘ธ๋“œ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์‹ ๊ทœ ๊ณ ๊ฐ ์ฒซ ์ฃผ๋ฌธ 3,000์› ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฟ ํฐ (์‹ ๊ทœ ๊ณ ๊ฐ) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ฒซ ์ฃผ๋ฌธ ์‹œ 3,000์› ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋ฐฐ๋‹ฌ์•ฑ ์‹ ๊ทœ ๊ณ ๊ฐ ์ž๋™ ์ ์šฉ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 180๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 270,000์› +- **์˜ˆ์ƒ ROI**: 360% +- **์„ฑ๊ณต ์š”์ธ**: + - ์‹ ๊ทœ ๊ณ ๊ฐ ํ™•๋ณด + - ์žฌ๋ฐฉ๋ฌธ ์ „ํ™˜์œจ 30-40% + - ์ง„์ž… ์žฅ๋ฒฝ ๋‚ฎ์ถค +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 8-3. SNS ์ธ์ฆ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ํŒจ์ŠคํŠธํ‘ธ๋“œ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๋ฒ„๊ฑฐ ์‚ฌ์ง„ ์ธ์Šคํƒ€๊ทธ๋žจ ์˜ฌ๋ฆฌ๊ณ  ๊ฐ์žํŠ€๊น€ ๋ฌด๋ฃŒ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: SNS ๋ฐ”์ด๋Ÿด +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ธ์Šคํƒ€๊ทธ๋žจ ๋ฒ„๊ฑฐ ์‚ฌ์ง„ ์—…๋กœ๋“œ ์‹œ ๊ฐ์žํŠ€๊น€ 1๊ฐœ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ โ†’ ๋ฒ„๊ฑฐ ์‚ฌ์ง„ ์ดฌ์˜ โ†’ ์ธ์Šคํƒ€๊ทธ๋žจ ์—…๋กœ๋“œ + #OO๋ฒ„๊ฑฐ โ†’ ์ง์› ํ™•์ธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 250๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,600,000์› +- **์˜ˆ์ƒ ROI**: 340% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ… + - 1020์„ธ๋Œ€ ํƒ€๊ฒŸ + - ์ฆ‰์‹œ ๋ณด์ƒ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 8-4. ์Šคํƒฌํ”„ ์ ๋ฆฝ ๋ฌด๋ฃŒ ๋ฒ„๊ฑฐ + +- **์—…์ข…๋ช…**: ํŒจ์ŠคํŠธํ‘ธ๋“œ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๋ฒ„๊ฑฐ 8๊ฐœ ๋จน์œผ๋ฉด 1๊ฐœ ๋ฌด๋ฃŒ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์Šคํƒฌํ”„/ํฌ์ธํŠธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 8ํšŒ ๊ตฌ๋งค ์ ๋ฆฝ ์‹œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋ฒ„๊ฑฐ 1๊ฐœ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์•ฑ ๊ฐ€์ž… โ†’ ๊ตฌ๋งค ์‹œ ์ž๋™ ์ ๋ฆฝ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 280๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,750,000์› +- **์˜ˆ์ƒ ROI**: 390% +- **์„ฑ๊ณต ์š”์ธ**: + - ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ + - ๊ณ ๊ฐ ์ถฉ์„ฑ๋„ ๊ตฌ์ถ• + - ์•ฑ ์‚ฌ์šฉ ํ™œ์„ฑํ™” +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 8-5. ์›”๋“œ์ปต ์‹œ์ฆŒ ํŠน๋ณ„ ํ”„๋กœ๋ชจ์…˜ + +- **์—…์ข…๋ช…**: ํŒจ์ŠคํŠธํ‘ธ๋“œ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์›”๋“œ์ปต ์‘์›! ํ•œ๊ตญ ๊ณจ ๋„ฃ์œผ๋ฉด ๋ฒ„๊ฑฐ ๋ฐ˜๊ฐ’" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฐฝ์˜์  ์ด๋ฒคํŠธ (์ด์Šˆ ๋งˆ์ผ€ํŒ…) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ํ•œ๊ตญ ์ถ•๊ตฌํŒ€ ๊ณจ ๋„ฃ์€ ๋‚  ์ „ ๋ฒ„๊ฑฐ 50% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๊ฒฝ๊ธฐ ๋‹น์ผ ๋งค์žฅ ๋˜๋Š” ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ๊ณ ๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 400๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 5,500,000์› +- **์˜ˆ์ƒ ROI**: 290% +- **์„ฑ๊ณต ์š”์ธ**: + - ์ด์Šˆ ๋งˆ์ผ€ํŒ… (์›”๋“œ์ปต) + - ํ™”์ œ์„ฑ ํ™•๋ณด + - ๋Œ€๊ทœ๋ชจ ์ฐธ์—ฌ ์œ ๋„ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์›”๋“œ์ปต/์˜ฌ๋ฆผํ”ฝ ๋“ฑ ์Šคํฌ์ธ  ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„ + +--- + +## 9. ํ”ผ์ž ์ „๋ฌธ์  (Pizza Restaurant) + +### 9-1. ํ‰์ผ ํ”ผ์ž 1+1 + +- **์—…์ข…๋ช…**: ํ”ผ์ž ์ „๋ฌธ์  +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "ํ‰์ผ ํ”ผ์ž 1+1 (ํฌ์žฅ ์ฃผ๋ฌธ)" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ๋ฒˆ๋“ค ํ”„๋กœ๋ชจ์…˜ (1+1) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์›”-๊ธˆ ํŠน์ • ํ”ผ์ž 1๊ฐœ ์ฃผ๋ฌธ ์‹œ 1๊ฐœ ๋ฌด๋ฃŒ (ํฌ์žฅ๋งŒ ํ•ด๋‹น) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํฌ์žฅ ์ฃผ๋ฌธ ์‹œ ์ž๋™ ์ ์šฉ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 200๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 300,000์› +- **์˜ˆ์ƒ ROI**: 440% +- **์„ฑ๊ณต ์š”์ธ**: + - ํฌ์žฅ ์ฃผ๋ฌธ ํ™œ์„ฑํ™” (๋ฐฐ๋‹ฌ๋น„ ์ ˆ๊ฐ) + - ํ‰์ผ ๋น„์ˆ˜๊ธฐ ํ™œ์šฉ + - ๋†’์€ ๊ฐ€์น˜ ์ธ์‹ (1+1) +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํ‰์ผ) + +### 9-2. ๋ฐฐ๋‹ฌ์•ฑ ๋ฆฌ๋ทฐ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ํ”ผ์ž ์ „๋ฌธ์  +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๋ฆฌ๋ทฐ ์ž‘์„ฑํ•˜๊ณ  ๋‹ค์Œ ์ฃผ๋ฌธ ์ฝœ๋ผ 2L ๋ฌด๋ฃŒ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ๋ฆฌ๋ทฐ + ์ฟ ํฐ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ๋ฐฐ๋‹ฌ์•ฑ ๋ฆฌ๋ทฐ ์ž‘์„ฑ ์‹œ ๋‹ค์Œ ์ฃผ๋ฌธ ์ฝœ๋ผ 2L ๋ฌด๋ฃŒ ์ฟ ํฐ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ฃผ๋ฌธ โ†’ ๋ฆฌ๋ทฐ ์ž‘์„ฑ โ†’ ์ž๋™ ์ฟ ํฐ ๋ฐœ๊ธ‰ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 170๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 260,000์› +- **์˜ˆ์ƒ ROI**: 370% +- **์„ฑ๊ณต ์š”์ธ**: + - ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ + - ๋ฐฐ๋‹ฌ์•ฑ ์ˆœ์œ„ ์ƒ์Šน + - ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์ž… +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 9-3. ๊ฐ€์กฑ ๋‹จ์œ„ ํ• ์ธ + +- **์—…์ข…๋ช…**: ํ”ผ์ž ์ „๋ฌธ์  +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๊ฐ€์กฑ ํ”ผ์ž ํŒŒํ‹ฐ! 3ํŒ ์ด์ƒ ์ฃผ๋ฌธ ์‹œ 20% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ๋ฒˆ๋“ค ํ”„๋กœ๋ชจ์…˜ (๋Œ€๋Ÿ‰ ์ฃผ๋ฌธ) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ํ”ผ์ž 3ํŒ ์ด์ƒ ์ฃผ๋ฌธ ์‹œ 20% ํ• ์ธ + ์ฝœ๋ผ 2L ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋˜๋Š” ๋ฐฐ๋‹ฌ ์ฃผ๋ฌธ ์‹œ ์ž๋™ ์ ์šฉ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 220๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,700,000์› +- **์˜ˆ์ƒ ROI**: 360% +- **์„ฑ๊ณต ์š”์ธ**: + - ๊ฐ€์กฑ/ํŒŒํ‹ฐ ์ˆ˜์š” ํƒ€๊ฒŸ + - ๊ฐ๋‹จ๊ฐ€ ์ƒ์Šน + - ์ฃผ๋ง/๊ณตํœด์ผ ํ™œ์„ฑํ™” +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์ฃผ๋ง, ํŠนํžˆ ์–ด๋ฆฐ์ด๋‚ /ํฌ๋ฆฌ์Šค๋งˆ์Šค + +### 9-4. ์›”๊ฐ„ ์‹ ๋ฉ”๋‰ด ์ถœ์‹œ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ํ”ผ์ž ์ „๋ฌธ์  +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ด๋‹ฌ์˜ ์‹ ๋ฉ”๋‰ด ํ”ผ์ž ์ถœ์‹œ! SNS ๊ณต์œ ํ•˜๊ณ  15% ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: SNS ๋ฐ”์ด๋Ÿด + ํ• ์ธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์‹ ๋ฉ”๋‰ด ์ฃผ๋ฌธ + SNS ๊ณต์œ  ์‹œ 15% ํ• ์ธ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์‹ ๋ฉ”๋‰ด ์ฃผ๋ฌธ โ†’ SNS ๊ณต์œ  โ†’ ์ง์› ํ™•์ธ ๋˜๋Š” ์ž๋™ ์ ์šฉ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 240๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,650,000์› +- **์˜ˆ์ƒ ROI**: 350% +- **์„ฑ๊ณต ์š”์ธ**: + - ์‹ ๋ฉ”๋‰ด ๋น ๋ฅธ ์ธ์ง€๋„ + - ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ… + - ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (์›” 1ํšŒ) + +### 9-5. ์Šคํฌ์ธ  ์‹œ์ฆŒ ํŠน๋ณ„ ํ”„๋กœ๋ชจ์…˜ + +- **์—…์ข…๋ช…**: ํ”ผ์ž ์ „๋ฌธ์  +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์•ผ๊ตฌ ์‹œ์ฆŒ ์‘์›! ํ™ˆ๋Ÿฐ 1๊ฐœ๋‹น ํ”ผ์ž 1,000์› ํ• ์ธ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฐฝ์˜์  ์ด๋ฒคํŠธ (์ด์Šˆ ๋งˆ์ผ€ํŒ…) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ํ•ด๋‹น ๋‚  ํ™ˆ๋Ÿฐ ๊ฐœ์ˆ˜๋งŒํผ ํ”ผ์ž 1,000์›์”ฉ ํ• ์ธ (์ตœ๋Œ€ 5,000์›) +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๊ฒฝ๊ธฐ ๋‹น์ผ ์ฃผ๋ฌธ ์‹œ ์ž๋™ ์ ์šฉ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ๊ณ ๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 380๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 5,700,000์› +- **์˜ˆ์ƒ ROI**: 270% +- **์„ฑ๊ณต ์š”์ธ**: + - ์ด์Šˆ ๋งˆ์ผ€ํŒ… (์Šคํฌ์ธ ) + - ํ™”์ œ์„ฑ ํ™•๋ณด + - ๋ฐ˜๋ณต ์ฐธ์—ฌ ๊ฐ€๋Šฅ (์‹œ์ฆŒ ๋‚ด๋‚ด) +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ๋ด„-๊ฐ€์„ (์•ผ๊ตฌ ์‹œ์ฆŒ 3-10์›”) + +--- + +## 10. ๋ฐ”๋ฒ ํ/๊ณ ๊นƒ์ง‘ (Barbecue / Steak House) + +### 10-1. ํ‰์ผ ์ ์‹ฌ ํŠน๊ฐ€ + +- **์—…์ข…๋ช…**: ๋ฐ”๋ฒ ํ/๊ณ ๊นƒ์ง‘ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "ํ‰์ผ ๋Ÿฐ์น˜ ์‚ผ๊ฒน์‚ด ์„ธํŠธ 1์ธ 12,900์›" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์‹œ๊ฐ„๋Œ€๋ณ„ ํ• ์ธ +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์›”-๊ธˆ 11์‹œ-15์‹œ ์‚ผ๊ฒน์‚ด ์„ธํŠธ 1์ธ 12,900์› +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ํ•ด๋‹น ์‹œ๊ฐ„๋Œ€ ๋งค์žฅ ๋ฐฉ๋ฌธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 180๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 290,000์› +- **์˜ˆ์ƒ ROI**: 380% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋น„์ˆ˜๊ธฐ ์‹œ๊ฐ„๋Œ€ ํ™œ์šฉ + - ์ง์žฅ์ธ ์ ์‹ฌ ํƒ€๊ฒŸ + - ์„ธํŠธ ๊ตฌ์„ฑ์œผ๋กœ ๊ฐ๋‹จ๊ฐ€ ์œ ์ง€ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํ‰์ผ) + +### 10-2. ์†Œ์…œ ๋ฏธ๋””์–ด ๊ณ ๊ธฐ ์ธ์ฆ ์ด๋ฒคํŠธ + +- **์—…์ข…๋ช…**: ๋ฐ”๋ฒ ํ/๊ณ ๊นƒ์ง‘ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "๊ณ ๊ธฐ ๊ตฝ๋Š” ์˜์ƒ ๋ฆด์Šค ์˜ฌ๋ฆฌ๊ณ  ์†Œ์ฃผ 2๋ณ‘ ์„œ๋น„์Šค" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: SNS ๋ฐ”์ด๋Ÿด +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ธ์Šคํƒ€๊ทธ๋žจ ๋ฆด์Šค ์—…๋กœ๋“œ ์‹œ ์†Œ์ฃผ 2๋ณ‘ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ๋งค์žฅ ๋ฐฉ๋ฌธ โ†’ ๊ณ ๊ธฐ ๊ตฝ๋Š” ์˜์ƒ ์ดฌ์˜ โ†’ ๋ฆด์Šค ์—…๋กœ๋“œ + #OO๊ณ ๊นƒ์ง‘ โ†’ ์ง์› ํ™•์ธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ €๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 160๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 270,000์› +- **์˜ˆ์ƒ ROI**: 340% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ… (์˜์ƒ ์ฝ˜ํ…์ธ ) + - 2030์„ธ๋Œ€ ํƒ€๊ฒŸ + - ์ฆ‰์‹œ ๋ณด์ƒ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 10-3. ๋‹จ์ฒด ์˜ˆ์•ฝ ํ• ์ธ + +- **์—…์ข…๋ช…**: ๋ฐ”๋ฒ ํ/๊ณ ๊นƒ์ง‘ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "6์ธ ์ด์ƒ ๋‹จ์ฒด ์˜ˆ์•ฝ 15% ํ• ์ธ + ๋œ์žฅ์ฐŒ๊ฐœ ์„œ๋น„์Šค" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ๋ฒˆ๋“ค ํ”„๋กœ๋ชจ์…˜ (๊ทธ๋ฃน ํƒ€๊ฒŸ) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: 6์ธ ์ด์ƒ ์˜ˆ์•ฝ ์‹œ 15% ํ• ์ธ + ๋œ์žฅ์ฐŒ๊ฐœ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์ „ํ™” ๋˜๋Š” ๋„ค์ด๋ฒ„ ์˜ˆ์•ฝ (1์ผ ์ „๊นŒ์ง€) +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 230๋ช… (์•ฝ 40๊ทธ๋ฃน) +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,750,000์› +- **์˜ˆ์ƒ ROI**: 370% +- **์„ฑ๊ณต ์š”์ธ**: + - ๋‹จ์ฒด ๊ณ ๊ฐ ํ™•๋ณด (ํšŒ์‹/๋ชจ์ž„) + - ๊ฐ๋‹จ๊ฐ€ ์ƒ์Šน + - ์‚ฌ์ „ ์˜ˆ์•ฝ์œผ๋กœ ์ค€๋น„ ์šฉ์ด +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ (ํŠนํžˆ ์—ฐ๋ง ํšŒ์‹ ์‹œ์ฆŒ) + +### 10-4. ์ƒ์ผ ๊ณ ๊ฐ ํŠน๋ณ„ ์„œ๋น„์Šค + +- **์—…์ข…๋ช…**: ๋ฐ”๋ฒ ํ/๊ณ ๊นƒ์ง‘ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "์ƒ์ผ ๊ณ ๊ฐ๋‹˜๊ป˜ ๋“ฑ์‹ฌ 200g + ์ผ€์ดํฌ ์„œ๋น„์Šค" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ (๊ณ ๊ฐ ์„ธ๊ทธ๋จผํŠธ) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ƒ์ผ ๋‹น์›” ๋ฐฉ๋ฌธ ์‹œ ๋“ฑ์‹ฌ 200g + ๋ฏธ๋‹ˆ ์ผ€์ดํฌ ๋ฌด๋ฃŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์˜ˆ์•ฝ ๋˜๋Š” ๋ฐฉ๋ฌธ ์‹œ ์ƒ์ผ ๋ฉ˜ํŠธ โ†’ ์‹ ๋ถ„์ฆ ํ™•์ธ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ์ค‘๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 200๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 1,800,000์› +- **์˜ˆ์ƒ ROI**: 355% +- **์„ฑ๊ณต ์š”์ธ**: + - ๊ฐœ์ธํ™” ๋งˆ์ผ€ํŒ… + - ํŠน๋ณ„ํ•œ ๋‚  ๊ฒฝํ—˜ + - ์žฌ๋ฐฉ๋ฌธ + ์ž…์†Œ๋ฌธ +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ์—ฐ์ค‘ + +### 10-5. ํ”„๋ฆฌ๋ฏธ์—„ ํ•œ์šฐ ์‹œ์‹ํšŒ + +- **์—…์ข…๋ช…**: ๋ฐ”๋ฒ ํ/๊ณ ๊นƒ์ง‘ +- **์ด๋ฒคํŠธ ์ œ๋ชฉ**: "VIP ๊ณ ๊ฐ ์ดˆ๋Œ€ ํ”„๋ฆฌ๋ฏธ์—„ ํ•œ์šฐ ์‹œ์‹ํšŒ" +- **์ด๋ฒคํŠธ ์œ ํ˜•**: ์ฐฝ์˜์  ์ด๋ฒคํŠธ (VIP) +- **๊ฒฝํ’ˆ/ํ˜œํƒ**: ์ƒ์œ„ 30๋ช… VIP ๊ณ ๊ฐ ํ•œ์šฐ ์‹œ์‹ํšŒ ์ดˆ๋Œ€ + 25% ํ• ์ธ๊ถŒ +- **์ฐธ์—ฌ ๋ฐฉ๋ฒ•**: ์›” 2ํšŒ ์ด์ƒ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๋Œ€์ƒ ๊ฐœ๋ณ„ ์ดˆ๋Œ€ +- **์˜ˆ์‚ฐ ๊ทœ๋ชจ**: ๊ณ ๋น„์šฉ +- **์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜**: 30๋ช… +- **์˜ˆ์ƒ ๋น„์šฉ**: 6,200,000์› +- **์˜ˆ์ƒ ROI**: 250% +- **์„ฑ๊ณต ์š”์ธ**: + - VIP ๊ณ ๊ฐ ์ถฉ์„ฑ๋„ ๊ฐ•ํ™” + - ํ”„๋ฆฌ๋ฏธ์—„ ๋ฉ”๋‰ด ํ”„๋กœ๋ชจ์…˜ + - ์ž…์†Œ๋ฌธ ๋งˆ์ผ€ํŒ… +- **์ ์šฉ ์‹œ๊ธฐ/๊ณ„์ ˆ**: ํŠน๋ณ„ ์‹œ์ฆŒ (์„ค๋‚ /์ถ”์„ ์ „) + +--- + +## ๋ฐ์ดํ„ฐ ํ™œ์šฉ ๊ฐ€์ด๋“œ + +### AI ํ•™์Šต ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ + +๋ณธ ๋ฐ์ดํ„ฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ์‹œ์Šคํ…œ์— ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค: + +1. **๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ** + - ๊ฐ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ + - OpenAI Embeddings API๋ฅผ ํ†ตํ•ด ๋ฒกํ„ฐํ™” + - Pinecone์— ์ €์žฅํ•˜์—ฌ ์œ ์‚ฌ๋„ ๊ฒ€์ƒ‰ ์ง€์› + +2. **์œ ์‚ฌ ์ด๋ฒคํŠธ ๊ฒ€์ƒ‰** + - ์‚ฌ์šฉ์ž์˜ ๋งค์žฅ ์ •๋ณด(์—…์ข…, ์ง€์—ญ, ์˜ˆ์‚ฐ)๋ฅผ ์ฟผ๋ฆฌ๋กœ ์‚ฌ์šฉ + - ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ๋„ ๊ธฐ๋ฐ˜ ์ƒ์œ„ 5๊ฐœ ์œ ์‚ฌ ์ด๋ฒคํŠธ ์ถ”์ถœ + - Claude API ํ”„๋กฌํ”„ํŠธ์— ์ปจํ…์ŠคํŠธ๋กœ ์ œ๊ณต + +3. **์ถ”์ฒœ ์ •ํ™•๋„ ํ–ฅ์ƒ** + - ์‹ค์ œ ์„ฑ๊ณต ์‚ฌ๋ก€ ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ๋กœ ์‹ ๋ขฐ๋„ ํ–ฅ์ƒ + - ROI, ์ฐธ์—ฌ์ž ์ˆ˜ ๋“ฑ ๊ตฌ์ฒด์  ์ˆ˜์น˜๋กœ ํ˜„์‹ค์  ์˜ˆ์ธก ๊ฐ€๋Šฅ + - ๊ณ„์ ˆ์„ฑ, ์—…์ข… ํŠน์„ฑ ๋ฐ˜์˜์œผ๋กœ ๋งž์ถคํ˜• ์ถ”์ฒœ + +### ๋ฐ์ดํ„ฐ ํ™•์žฅ ๋ฐฉ์•ˆ + +์ดˆ๊ธฐ 50๊ฐœ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ™•์žฅ ๊ฐ€๋Šฅ: + +1. **์ž์‚ฌ ๋ฐ์ดํ„ฐ ์ถ•์ ** + - ์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑํ•œ ์ด๋ฒคํŠธ ์ž๋™ ์ˆ˜์ง‘ + - ์‹ค์ œ ์„ฑ๊ณผ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ + - ํ”ผ๋“œ๋ฐฑ ๋ฐ˜์˜ํ•œ ์ •ํ™•๋„ ๊ฐœ์„  + +2. **์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘** + - SNS ํฌ๋กค๋ง (๋„ค์ด๋ฒ„ ๋ธ”๋กœ๊ทธ, ์ธ์Šคํƒ€๊ทธ๋žจ) + - ๊ณต๊ณต๋ฐ์ดํ„ฐ API ํ™œ์šฉ + - ๋ฐฐ๋‹ฌ ํ”Œ๋žซํผ ์ด๋ฒคํŠธ ๋ถ„์„ + +3. **์—…์ข… ํ™•์žฅ** + - ์ถ”๊ฐ€ ์™ธ์‹์—… ์นดํ…Œ๊ณ ๋ฆฌ (ํƒœ๊ตญ, ๋ฒ ํŠธ๋‚จ, ๋””์ €ํŠธ ์ „๋ฌธ์  ๋“ฑ) + - ๋น„์™ธ์‹์—… ์†Œ์ƒ๊ณต์ธ (์†Œ๋งค, ์„œ๋น„์Šค์—…) + - ํ”„๋žœ์ฐจ์ด์ฆˆ vs ๊ฐœ์ธ ์‚ฌ์—…์ž ๊ตฌ๋ถ„ + +--- + +## ์ฐธ๊ณ  ๋ฌธํ—Œ + +- ์ž๋‹ด์น˜ํ‚จ ๋งค์ถœ์ƒ์ƒ ํ”„๋กœ์ ํŠธ ์‚ฌ๋ก€ (2023) +- ํฌ์น˜์น˜ํ‚จ ๋ธŒ๋žœ๋“œ ์ „ํ™˜ ์‚ฌ๋ก€ +- ํ™ˆํ”Œ๋Ÿฌ์Šค ๋‹น๋‹น์น˜ํ‚จ 710๋งŒํŒฉ ํŒ๋งค ์‚ฌ๋ก€ +- ์•„๋œจ๋ฒ  ๋ฒ ์ด์ปค๋ฆฌ 6๋…„ ์„ฑ์žฅ ์‚ฌ๋ก€ +- ๊น€ํ˜„์ˆ™, ์‹ฌ์„ฑ์šฑ, ๊น€์šดํ•œ (2011). ํ•œ๊ตญ๊ด‘๊ณ ํ™๋ณดํ•™๋ณด. ์ฒดํ—˜ ๋งˆ์ผ€ํŒ… ์—ฐ๊ตฌ +- ์ •๋ฏผ์„œ ์™ธ (2017). ๋น…๋ฐ์ดํ„ฐํ•™ํšŒ. ํ”„๋กœ๋ชจ์…˜ ํƒ€์ด๋ฐ ์—ฐ๊ตฌ +- ์—„ํ•ด์ •, ์ง„ํ˜„์ • (2024). ํ˜ธํ…”๊ฒฝ์˜ํ•™์—ฐ๊ตฌ. ๋ฆฌ๋ทฐ ๋งˆ์ผ€ํŒ… ์—ฐ๊ตฌ +- ํ•œ๊ตญ์™ธ์‹์‚ฐ์—…๊ฒฝ์˜์—ฐ๊ตฌ์› (2024-2025). ์™ธ์‹ ํŠธ๋ Œ๋“œ ๋ณด๊ณ ์„œ + +--- + +**๋ฌธ์„œ ๋** diff --git a/design/aidata/์ง€์—ญ๋ณ„_ํŠธ๋ Œ๋“œ_๋ฐ์ดํ„ฐ.md b/design/aidata/์ง€์—ญ๋ณ„_ํŠธ๋ Œ๋“œ_๋ฐ์ดํ„ฐ.md new file mode 100644 index 0000000..cdeab84 --- /dev/null +++ b/design/aidata/์ง€์—ญ๋ณ„_ํŠธ๋ Œ๋“œ_๋ฐ์ดํ„ฐ.md @@ -0,0 +1,955 @@ +# ์ง€์—ญ๋ณ„ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ + +**์ž‘์„ฑ์ผ**: 2025-10-21 +**๋ฒ„์ „**: 1.0 +**๋ชฉ์ **: AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ์‹œ๊ตฐ๊ตฌ๋ณ„ ์ƒ๊ถŒ ํŠธ๋ Œ๋“œ ๋ฐ ์ด๋ฒคํŠธ ์„ฑ๊ณต ํŒจํ„ด ๋ฐ์ดํ„ฐ + +--- + +## ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์„ค๋ช… + +๊ฐ ์‹œ๊ตฐ๊ตฌ๋ณ„ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ๋Š” ๋‹ค์Œ ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค: + +- **์ง€์—ญ ํŠน์„ฑ**: ์ƒ๊ถŒ ์œ ํ˜•, ์ฃผ์š” ๊ณ ๊ฐ์ธต, ์œ ๋™์ธ๊ตฌ ํŠน์ง• +- **์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: ์Œ์‹์ , ์†Œ๋งค์ , ์„œ๋น„์Šค์—… ๋“ฑ ์—…์ข…๋ณ„ ์„ฑ๊ณต ํŒจํ„ด +- **๊ณ„์ ˆ๋ณ„ ์†Œ๋น„ ํŒจํ„ด**: ์‹œ์ฆŒ๋ณ„ ์†Œ๋น„ ํŠน์„ฑ ๋ฐ ์ด๋ฒคํŠธ ์ ๊ธฐ +- **์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: ์‹ค์ œ ์„ฑ๊ณตํ•œ ์ด๋ฒคํŠธ ์œ ํ˜• ๋ฐ ROI +- **์ถ”์ฒœ ์ „๋žต**: AI๊ฐ€ ํ™œ์šฉํ•  ์ด๋ฒคํŠธ ์„ค๊ณ„ ์ธ์‚ฌ์ดํŠธ + +--- + +## ์„œ์šธํŠน๋ณ„์‹œ + +### ์ข…๋กœ๊ตฌ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๊ด€๊ด‘์ง€ + ์ „ํ†ต์‹œ์žฅ + ์˜คํ”ผ์Šค +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ด€๊ด‘๊ฐ(40%), ์ง์žฅ์ธ(35%), ์ง€์—ญ์ฃผ๋ฏผ(25%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 32๋งŒ๋ช…, ์ฃผ๋ง 45๋งŒ๋ช… +- ํŠน์ง•: ์ธ์‚ฌ๋™, ๊ด‘ํ™”๋ฌธ ์˜คํ”ผ์Šค, ๋ถ์ดŒ ํ•œ์˜ฅ๋งˆ์„ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ•œ์‹): ์ „ํ†ต ํ•œ์ •์‹, ๊ถ์ค‘์š”๋ฆฌ ์ฒดํ—˜ํ˜• ์ด๋ฒคํŠธ ํšจ๊ณผ์  + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 25,000์› + - ์žฌ๋ฐฉ๋ฌธ์œจ: 35% + - ์™ธ๊ตญ์ธ ๋น„์œจ: 45% +- ์นดํŽ˜: ์ „ํ†ต ์ฐป์ง‘ + ํ˜„๋Œ€์‹ ์นดํŽ˜ ๊ณต์กด + - SNS ๋ฐ”์ด๋Ÿด ์ค‘์š”๋„: ์ƒ + - ์ธ์Šคํƒ€๊ทธ๋ž˜๋จธ๋ธ” ์š”์†Œ ํ•„์ˆ˜ + +**๊ณ„์ ˆ๋ณ„ ์†Œ๋น„ ํŒจํ„ด**: +- ๋ด„(3-5์›”): ์™ธ๊ตญ์ธ ๊ด€๊ด‘๊ฐ ๊ธ‰์ฆ (+40%), ํ•œ๋ณต ์ฒดํ—˜ ์—ฐ๊ณ„ ์ด๋ฒคํŠธ +- ์—ฌ๋ฆ„(6-8์›”): ๋ƒ‰๋ฉด/๋น™์ˆ˜ ์ด๋ฒคํŠธ, ์•ผ๊ฐ„ ๊ด€๊ด‘ ํƒ€๊ฒŸ +- ๊ฐ€์„(9-11์›”): ๋‹จํ’ ์‹œ์ฆŒ ํŒจํ‚ค์ง€ ์ด๋ฒคํŠธ (+35% ๋งค์ถœ) +- ๊ฒจ์šธ(12-2์›”): ์ „ํ†ต์ฃผ ์ด๋ฒคํŠธ, ๋”ฐ๋œปํ•œ ๊ตญ๋ฌผ ์š”๋ฆฌ + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๋ฌธํ™” ์ฒดํ—˜ ์—ฐ๊ณ„", + "title": "ํ•œ๋ณต ์ž…๊ณ  ์˜ค์‹œ๋ฉด ํ•œ์ •์‹ 20% ํ• ์ธ", + "์—…์ข…": "ํ•œ์‹๋‹น", + "๊ธฐ๊ฐ„": "2์ฃผ", + "์˜ˆ์‚ฐ": "500,000์›", + "์ฐธ์—ฌ์ž": "280๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "450%", + "ROI": "520%" +} +``` + +**์ถ”์ฒœ ์ „๋žต**: +- ์™ธ๊ตญ์ธ ํƒ€๊ฒŸ: ์˜์–ด/์ผ์–ด/์ค‘๊ตญ์–ด ๋ฉ”๋‰ด + ๋ฌธํ™” ์ฒดํ—˜ ์—ฐ๊ณ„ +- ์ง์žฅ์ธ ํƒ€๊ฒŸ: ์ ์‹ฌ ํŠน๊ฐ€(11:30-13:30), ์ฃผ์ค‘ ํ• ์ธ +- ๊ด€๊ด‘๊ฐ ํƒ€๊ฒŸ: SNS ์ธ์ฆ ์ด๋ฒคํŠธ, ์‚ฌ์ง„ ์ดฌ์˜ ์ŠคํŒŸ ์ œ๊ณต + +--- + +### ์ค‘๊ตฌ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๊ธˆ์œตยท์ƒ์—… ์ค‘์‹ฌ์ง€ + ๋ช…๋™ ๊ด€๊ด‘ํŠน๊ตฌ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ์ง์žฅ์ธ(50%), ๊ด€๊ด‘๊ฐ(35%), ์‡ผํ•‘๊ฐ(15%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 50๋งŒ๋ช…, ์ฃผ๋ง 65๋งŒ๋ช… +- ํŠน์ง•: ๋ช…๋™ ํŒจ์…˜๊ฑฐ๋ฆฌ, ์„์ง€๋กœ ๊ธˆ์œต๊ฐ€, ๋‚จ๋Œ€๋ฌธ์‹œ์žฅ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ“จ์ „/์™ธ์‹): ๋น ๋ฅธ ํšŒ์ „์œจ, ํŠธ๋ Œ๋””ํ•œ ๋ฉ”๋‰ด + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 15,000์› + - ํšŒ์ „์œจ: 1.5ํšŒ/ํ…Œ์ด๋ธ” + - ๋ฐฐ๋‹ฌ ๋น„์ค‘: 25% +- ํ™”์žฅํ’ˆ/๋ทฐํ‹ฐ: K-๋ทฐํ‹ฐ ์ฒดํ—˜ํ˜• ์ด๋ฒคํŠธ + - ์™ธ๊ตญ์ธ ๋งค์ถœ ๋น„์ค‘: 60% + +**๊ณ„์ ˆ๋ณ„ ์†Œ๋น„ ํŒจํ„ด**: +- ๋ด„: ์ค‘๊ตญ์ธ ๋‹จ์ฒด ๊ด€๊ด‘๊ฐ (+50%), ์„ธํŠธ ๋ฉ”๋‰ด ์ธ๊ธฐ +- ์—ฌ๋ฆ„: ๋ƒ‰๋ฐฉ ๋งค์žฅ ์„ ํ˜ธ, ์‹œ์›ํ•œ ์Œ๋ฃŒ ์ด๋ฒคํŠธ +- ๊ฐ€์„: ์‡ผํ•‘ ์‹œ์ฆŒ ์—ฐ๊ณ„ ์ด๋ฒคํŠธ ํšจ๊ณผ์  +- ๊ฒจ์šธ: ํฌ๋ฆฌ์Šค๋งˆ์Šค ํŠน์ˆ˜, ์ปคํ”Œ ํŒจํ‚ค์ง€ + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "์‹œ๊ฐ„๋Œ€๋ณ„ ํ• ์ธ", + "title": "๋Ÿฐ์น˜ํƒ€์ž„ ์„ธํŠธ ๋ฉ”๋‰ด 30% ํ• ์ธ", + "์—…์ข…": "ํ“จ์ „ ๋ ˆ์Šคํ† ๋ž‘", + "๊ธฐ๊ฐ„": "1๊ฐœ์›”", + "์˜ˆ์‚ฐ": "1,200,000์›", + "์ฐธ์—ฌ์ž": "1,850๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "320%", + "ROI": "480%" +} +``` + +**์ถ”์ฒœ ์ „๋žต**: +- ์ง์žฅ์ธ: ๋น ๋ฅธ ์„œ๋น„์Šค + ๊ฐ€์„ฑ๋น„ ์„ธํŠธ๋ฉ”๋‰ด +- ๊ด€๊ด‘๊ฐ: ํฌํ† ์กด + SNS ์ด๋ฒคํŠธ + ๋‹ค๊ตญ์–ด ์•ˆ๋‚ด +- ์‡ผํ•‘๊ฐ: ์˜์ˆ˜์ฆ ํ•ฉ์‚ฐ ํ• ์ธ, ์ธ๊ทผ ๋งค์žฅ ์ œํœด + +--- + +### ๊ฐ•๋‚จ๊ตฌ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ํ”„๋ฆฌ๋ฏธ์—„ ์ƒ์—…์ง€๊ตฌ + ์˜คํ”ผ์Šค +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ์ง์žฅ์ธ(45%), ๊ณ ์†Œ๋“์ธต(30%), MZ์„ธ๋Œ€(25%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 80๋งŒ๋ช…, ์ฃผ๋ง 55๋งŒ๋ช… +- ํŠน์ง•: ๊ฐ•๋‚จ์—ญ, ์‹ ๋…ผํ˜„์—ญ, ์ฒญ๋‹ด๋™ ๋ช…ํ’ˆ๊ฑฐ๋ฆฌ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (๊ณ ๊ธ‰/ํ”„๋ฆฌ๋ฏธ์—„): ํ’ˆ์งˆ ์ค‘์‹œ, ํ”„๋ผ์ด๋ฒ„์‹œ, ์ฐจ๋ณ„ํ™”๋œ ๊ฒฝํ—˜ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 45,000์› + - ์žฌ๋ฐฉ๋ฌธ์œจ: 55% + - ์˜ˆ์•ฝ๋ฅ : 70% +- ์นดํŽ˜(ํ”„๋ฆฌ๋ฏธ์—„): ๋ธŒ๋žœ๋“œ ๊ฐ€์น˜, ์ธํ…Œ๋ฆฌ์–ด, ๋…ํŠนํ•œ ๋ฉ”๋‰ด + - ๊ฐ๋‹จ๊ฐ€: 8,000์› + - SNS ์ค‘์š”๋„: ์ตœ์ƒ + +**๊ณ„์ ˆ๋ณ„ ์†Œ๋น„ ํŒจํ„ด**: +- ๋ด„: ์•ผ์™ธ ํ…Œ๋ผ์Šค ์ด๋ฒคํŠธ, ๋ธŒ๋Ÿฐ์น˜ ์„ธํŠธ +- ์—ฌ๋ฆ„: ํ”„๋ฆฌ๋ฏธ์—„ ๋””์ €ํŠธ, ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์Œ๋ฃŒ +- ๊ฐ€์„: ์™€์ธ ํŽ˜์–ด๋ง ์ด๋ฒคํŠธ, ์‹œ์ฆŒ ํ•œ์ • ๋ฉ”๋‰ด +- ๊ฒจ์šธ: VIP ์ดˆ๋Œ€ ์ด๋ฒคํŠธ, ์—ฐ๋ง ๋ชจ์ž„ ํŒจํ‚ค์ง€ + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "VIP ๋ฉค๋ฒ„์‹ญ", + "title": "์›”๊ฐ„ ์™€์ธ ํŽ˜์–ด๋ง ํด๋ž˜์Šค + 10% ํ• ์ธ", + "์—…์ข…": "ํŒŒ์ธ๋‹ค์ด๋‹", + "๊ธฐ๊ฐ„": "3๊ฐœ์›”", + "์˜ˆ์‚ฐ": "3,500,000์›", + "์ฐธ์—ฌ์ž": "120๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "280%", + "ROI": "420%", + "LTV_์ฆ๊ฐ€": "+65%" +} +``` + +**์ถ”์ฒœ ์ „๋žต**: +- ๊ณ ์†Œ๋“์ธต: VIP ํ”„๋กœ๊ทธ๋žจ, ๋…์  ๊ฒฝํ—˜, ํ”„๋ฆฌ๋ฏธ์—„ ํ˜œํƒ +- ์ง์žฅ์ธ: ๋น„์ฆˆ๋‹ˆ์Šค ๋Ÿฐ์น˜ ์„ธํŠธ, ํšŒ์‹ ํŒจํ‚ค์ง€ +- MZ์„ธ๋Œ€: ์ธ์Šคํƒ€๊ทธ๋ž˜๋จธ๋ธ” ๋ฉ”๋‰ด, ํŒ์—… ์ด๋ฒคํŠธ + +--- + +### ์†กํŒŒ๊ตฌ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๋Œ€๊ทœ๋ชจ ์ƒ์—…์‹œ์„ค + ์ฃผ๊ฑฐ์ง€์—ญ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ฐ€์กฑ(40%), ์ง์žฅ์ธ(30%), ์ Š์€์ธต(30%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 35๋งŒ๋ช…, ์ฃผ๋ง 60๋งŒ๋ช… +- ํŠน์ง•: ๋กฏ๋ฐ์›”๋“œ, ์ž ์‹ค์—ญ ์ƒ๊ถŒ, ์˜ฌ๋ฆผํ”ฝ๊ณต์› + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํŒจ๋ฐ€๋ฆฌ): ๋„“์€ ๊ณต๊ฐ„, ํ‚ค์ฆˆ ๋ฉ”๋‰ด, ์ฃผ์ฐจ ํŽธ์˜ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 35,000์› (๊ฐ€์กฑ ๋‹จ์œ„) + - ์ฃผ๋ง ๋งค์ถœ ๋น„์ค‘: 65% + - ์–ด๋ฆฐ์ด ๋™๋ฐ˜: 70% +- ๋ฒ ์ด์ปค๋ฆฌ/๋””์ €ํŠธ: ํ…Œ์ดํฌ์•„์›ƒ ์ค‘์‹ฌ, ์„ ๋ฌผ์šฉ ์ˆ˜์š” + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 25,000์› + +**๊ณ„์ ˆ๋ณ„ ์†Œ๋น„ ํŒจํ„ด**: +- ๋ด„: ์•ผ์™ธ ํ™œ๋™ ์ฆ๊ฐ€, ํ”ผํฌ๋‹‰ ํŒจํ‚ค์ง€ +- ์—ฌ๋ฆ„: ๋กฏ๋ฐ์›”๋“œ ์—ฐ๊ณ„, ํ‚ค์ฆˆ ์ด๋ฒคํŠธ +- ๊ฐ€์„: ์šด๋™ํšŒ ์‹œ์ฆŒ, ๋‹จ์ฒด ์ฃผ๋ฌธ ์ฆ๊ฐ€ +- ๊ฒจ์šธ: ์‹ค๋‚ด ํ™œ๋™ ์ฆ๊ฐ€, ์ƒ์ผํŒŒํ‹ฐ ํŒจํ‚ค์ง€ + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๊ฐ€์กฑ ํŒจํ‚ค์ง€", + "title": "๋กฏ๋ฐ์›”๋“œ ํ‹ฐ์ผ“ ์ธ์ฆ ์‹œ ํ‚ค์ฆˆ ๋ฉ”๋‰ด ๋ฌด๋ฃŒ", + "์—…์ข…": "ํŒจ๋ฐ€๋ฆฌ ๋ ˆ์Šคํ† ๋ž‘", + "๊ธฐ๊ฐ„": "2๊ฐœ์›”", + "์˜ˆ์‚ฐ": "2,000,000์›", + "์ฐธ์—ฌ์ž": "680๊ฐ€์กฑ", + "๋งค์ถœ_์ฆ๊ฐ€": "380%", + "ROI": "550%" +} +``` + +**์ถ”์ฒœ ์ „๋žต**: +- ๊ฐ€์กฑ: ํ‚ค์ฆˆ์กด, ๋ฌด๋ฃŒ ์Œ๋ฃŒ ๋ฆฌํ•„, ์ƒ์ผ ์ด๋ฒคํŠธ +- ์ง์žฅ์ธ: ํ…Œ์ดํฌ์•„์›ƒ ํ• ์ธ, ๋ฐฐ๋‹ฌ ํ”„๋กœ๋ชจ์…˜ +- ๊ด€๊ด‘๊ฐ: ๋กฏ๋ฐ์›”๋“œ ์—ฐ๊ณ„ ํ• ์ธ, ๊ธฐ๋…ํ’ˆ ์ฆ์ • + +--- + +## ๋ถ€์‚ฐ๊ด‘์—ญ์‹œ + +### ํ•ด์šด๋Œ€๊ตฌ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๊ด€๊ด‘ ํŠนํ™” ์ง€์—ญ + ํ•ด์–‘ ๋ฆฌ์กฐํŠธ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ด€๊ด‘๊ฐ(60%), ์ง€์—ญ์ฃผ๋ฏผ(25%), ์™ธ๊ตญ์ธ(15%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 25๋งŒ๋ช…, ์ฃผ๋ง/์„ฑ์ˆ˜๊ธฐ 80๋งŒ๋ช… +- ํŠน์ง•: ํ•ด์šด๋Œ€ ํ•ด์ˆ˜์š•์žฅ, ๋™๋ฐฑ์„ฌ, ๋งˆ๋ฆฐ์‹œํ‹ฐ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ•ด์‚ฐ๋ฌผ/ํšŸ์ง‘): ์‹ ์„ ๋„ ๊ฐ•์กฐ, ๊ณ„์ ˆ ๋ฉ”๋‰ด + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 40,000์› + - ์™ธ์ง€์ธ ๋น„์œจ: 75% + - ์˜ˆ์•ฝ๋ฅ : 60% +- ์นดํŽ˜(์˜ค์…˜๋ทฐ): ๋ทฐ ๋ง›์ง‘, ํฌํ† ์กด + - ๊ฐ๋‹จ๊ฐ€: 7,000์› + - ์ฒด๋ฅ˜์‹œ๊ฐ„: ํ‰๊ท  90๋ถ„ + +**๊ณ„์ ˆ๋ณ„ ์†Œ๋น„ ํŒจํ„ด**: +- ๋ด„: ๋ฒš๊ฝƒ ์‹œ์ฆŒ, ์•ผ์™ธ ํ…Œ๋ผ์Šค (+30%) +- ์—ฌ๋ฆ„: ์„ฑ์ˆ˜๊ธฐ ํ”ผํฌ, ํ•ด์‚ฐ๋ฌผ ์ด๋ฒคํŠธ (+150%) +- ๊ฐ€์„: ์˜ํ™”์ œ ์‹œ์ฆŒ, ๊ณ ๊ธ‰ ๋‹ค์ด๋‹ ์ˆ˜์š” +- ๊ฒจ์šธ: ๋น„์ˆ˜๊ธฐ ํ• ์ธ, ์ง€์—ญ์ฃผ๋ฏผ ํƒ€๊ฒŸ + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๊ณ„์ ˆ ํ•œ์ •", + "title": "์—ฌ๋ฆ„ ํŠน์„  ํ•ด์‚ฐ๋ฌผ ์„ธํŠธ + ์˜ค์…˜๋ทฐ ์„ ์šฐ์„  ๋ฐฐ์ •", + "์—…์ข…": "ํ•ด์‚ฐ๋ฌผ ๋ ˆ์Šคํ† ๋ž‘", + "๊ธฐ๊ฐ„": "3๊ฐœ์›” (6-8์›”)", + "์˜ˆ์‚ฐ": "4,500,000์›", + "์ฐธ์—ฌ์ž": "1,250๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "520%", + "ROI": "680%" +} +``` + +**์ถ”์ฒœ ์ „๋žต**: +- ๊ด€๊ด‘๊ฐ: SNS ์ธ์ฆ ์ด๋ฒคํŠธ, ํฌํ† ์กด, ๊ธฐ๋…ํ’ˆ +- ์™ธ๊ตญ์ธ: ์˜์–ด/์ค‘๊ตญ์–ด/์ผ์–ด ๋ฉ”๋‰ด, ๋ฌธํ™” ์ฒดํ—˜ +- ์ง€์—ญ์ฃผ๋ฏผ: ๋น„์ˆ˜๊ธฐ ํ• ์ธ, ๋กœ์ปฌ ๋ฉค๋ฒ„์‹ญ + +--- + +### ๋ถ€์‚ฐ์ง„๊ตฌ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ์ƒ์—… ์ค‘์‹ฌ์ง€ + ๊ตํ†ต ํ—ˆ๋ธŒ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ์ง์žฅ์ธ(45%), ๋Œ€ํ•™์ƒ(30%), ์‡ผํ•‘๊ฐ(25%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 45๋งŒ๋ช…, ์ฃผ๋ง 50๋งŒ๋ช… +- ํŠน์ง•: ์„œ๋ฉด ์ƒ๊ถŒ, ๋ถ€์‚ฐ์—ญ, ๋Œ€ํ•™๊ฐ€ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ•œ์‹/๋ถ„์‹): ๊ฐ€์„ฑ๋น„ ์ค‘์‹œ, ๋น ๋ฅธ ํšŒ์ „ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 9,000์› + - ํšŒ์ „์œจ: 2ํšŒ/ํ…Œ์ด๋ธ” + - ๋ฐฐ๋‹ฌ ๋น„์ค‘: 40% +- ์นดํŽ˜(์Šคํ„ฐ๋””): ์žฅ์‹œ๊ฐ„ ์ฒด๋ฅ˜, ์กฐ์šฉํ•œ ๋ถ„์œ„๊ธฐ + - ๊ฐ๋‹จ๊ฐ€: 5,000์› + - ์ฒด๋ฅ˜์‹œ๊ฐ„: 3-4์‹œ๊ฐ„ + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "ํ•™์ƒ ํ• ์ธ", + "title": "ํ•™์ƒ์ฆ ์ œ์‹œ ์‹œ 20% ํ• ์ธ + ์Œ๋ฃŒ ๋ฆฌํ•„ ๋ฌด๋ฃŒ", + "์—…์ข…": "์นดํŽ˜", + "๊ธฐ๊ฐ„": "ํ•™๊ธฐ ์ค‘ 4๊ฐœ์›”", + "์˜ˆ์‚ฐ": "800,000์›", + "์ฐธ์—ฌ์ž": "2,100๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "280%", + "ROI": "450%" +} +``` + +--- + +## ๋Œ€๊ตฌ๊ด‘์—ญ์‹œ + +### ์ค‘๊ตฌ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๊ตฌ๋„์‹ฌ + ์ „ํ†ต์‹œ์žฅ + ์ฒญ๋…„๋ชฐ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ์ง€์—ญ์ฃผ๋ฏผ(50%), ์ฒญ๋…„(30%), ๊ด€๊ด‘๊ฐ(20%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 30๋งŒ๋ช…, ์ฃผ๋ง 38๋งŒ๋ช… +- ํŠน์ง•: ๋™์„ฑ๋กœ, ์„œ๋ฌธ์‹œ์žฅ, ์•ฝ๋ น์‹œ์žฅ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ–ฅํ† ์Œ์‹): ์ „ํ†ต ๋Œ€๊ตฌ ์š”๋ฆฌ, ์ง€์—ญ ํŠน์‚ฐ๋ฌผ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 12,000์› + - ์žฌ๋ฐฉ๋ฌธ์œจ: 60% (์ง€์—ญ๋ฏผ) + - ๊ด€๊ด‘๊ฐ ๋น„์œจ: 25% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๋กœ์ปฌ ํ˜‘์—…", + "title": "์„œ๋ฌธ์‹œ์žฅ ์˜์ˆ˜์ฆ ํ•ฉ์‚ฐ ์ด๋ฒคํŠธ", + "์—…์ข…": "ํ•œ์‹๋‹น", + "๊ธฐ๊ฐ„": "1๊ฐœ์›”", + "์˜ˆ์‚ฐ": "600,000์›", + "์ฐธ์—ฌ์ž": "520๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "340%", + "ROI": "490%" +} +``` + +--- + +## ์ธ์ฒœ๊ด‘์—ญ์‹œ + +### ์ค‘๊ตฌ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๊ตญ์ œ๊ณตํ•ญ + ์ฐจ์ด๋‚˜ํƒ€์šด + ์›”๋ฏธ๋„ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ด€๊ด‘๊ฐ(50%), ์™ธ๊ตญ์ธ(25%), ์ง€์—ญ์ฃผ๋ฏผ(25%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 40๋งŒ๋ช…, ์ฃผ๋ง 65๋งŒ๋ช… +- ํŠน์ง•: ์ธ์ฒœ๊ณตํ•ญ, ์ฐจ์ด๋‚˜ํƒ€์šด, ๊ฐœํ•ญ์žฅ ๋ฌธํ™”์ง€๊ตฌ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (์ค‘์‹): ์งœ์žฅ๋ฉด/์งฌ๋ฝ• ํŠนํ™”, SNS ํ•ซํ”Œ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 15,000์› + - ์™ธ์ง€์ธ ๋น„์œจ: 70% + - ํฌ์žฅ/๋ฐฐ๋‹ฌ: 35% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "SNS ๋ฐ”์ด๋Ÿด", + "title": "์ธ์ƒ์ƒท ์ŠคํŒŸ ์ธ์ฆ ์‹œ ์งœ์žฅ๋ฉด ๋ฌด๋ฃŒ ๊ณฑ๋นผ๊ธฐ", + "์—…์ข…": "์ค‘์‹๋‹น", + "๊ธฐ๊ฐ„": "2์ฃผ", + "์˜ˆ์‚ฐ": "450,000์›", + "์ฐธ์—ฌ์ž": "380๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "410%", + "ROI": "580%", + "SNS_๋„๋‹ฌ": "12๋งŒ๋ช…" +} +``` + +--- + +## ๊ด‘์ฃผ๊ด‘์—ญ์‹œ + +### ๋™๊ตฌ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ์˜ˆ์ˆ ๋ฌธํ™” ์ค‘์‹ฌ์ง€ + ์ „ํ†ต์‹œ์žฅ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๋ฌธํ™”์• ํ˜ธ๊ฐ€(35%), ์ง€์—ญ์ฃผ๋ฏผ(40%), ๋Œ€ํ•™์ƒ(25%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 22๋งŒ๋ช…, ์ฃผ๋ง 30๋งŒ๋ช… +- ํŠน์ง•: ์•„์‹œ์•„๋ฌธํ™”์ „๋‹น, ์ถฉ์žฅ๋กœ, ๋™๋ช…๋™ ์นดํŽ˜๊ฑฐ๋ฆฌ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์นดํŽ˜(๊ฐ์„ฑ): ๋…๋ฆฝ์„œ์  ์—ฐ๊ณ„, ์ „์‹œ ๊ณต๊ฐ„ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 6,500์› + - ์ฒด๋ฅ˜์‹œ๊ฐ„: 120๋ถ„ + - ๋ฌธํ™” ์ด๋ฒคํŠธ ์—ฐ๊ณ„ ํšจ๊ณผ: +45% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๋ฌธํ™” ํ˜‘์—…", + "title": "์ „์‹œ ํ‹ฐ์ผ“ ์ธ์ฆ ์‹œ ์Œ๋ฃŒ 1+1", + "์—…์ข…": "์นดํŽ˜", + "๊ธฐ๊ฐ„": "์ „์‹œ ๊ธฐ๊ฐ„ 2๊ฐœ์›”", + "์˜ˆ์‚ฐ": "700,000์›", + "์ฐธ์—ฌ์ž": "650๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "320%", + "ROI": "470%" +} +``` + +--- + +## ๋Œ€์ „๊ด‘์—ญ์‹œ + +### ์œ ์„ฑ๊ตฌ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๋Œ€ํ•™๊ฐ€ + ์—ฐ๊ตฌ๋‹จ์ง€ + ์˜จ์ฒœ ๊ด€๊ด‘ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๋Œ€ํ•™์ƒ(40%), ์—ฐ๊ตฌ์›(30%), ๊ด€๊ด‘๊ฐ(30%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 35๋งŒ๋ช…, ์ฃผ๋ง 42๋งŒ๋ช… +- ํŠน์ง•: KAIST, ์ถฉ๋‚จ๋Œ€, ๋Œ€๋•์—ฐ๊ตฌ๋‹จ์ง€, ์œ ์„ฑ์˜จ์ฒœ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ“จ์ „/์ฐฝ์—…): ์‹คํ—˜์  ๋ฉ”๋‰ด, ํŠธ๋ Œ๋””ํ•œ ์ฝ˜์…‰ํŠธ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 13,000์› + - ํ•™์ƒ ๋น„์œจ: 55% + - SNS ๋งˆ์ผ€ํŒ… ํšจ๊ณผ: ์ƒ + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "ํ•™์ƒ ํƒ€๊ฒŸ", + "title": "์‹œํ—˜๊ธฐ๊ฐ„ ํŠน๋ณ„ ์ด๋ฒคํŠธ: ์•ผ์‹ ์„ธํŠธ 30% ํ• ์ธ", + "์—…์ข…": "๋ถ„์‹/์•ผ์‹", + "๊ธฐ๊ฐ„": "์ค‘๊ฐ„ยท๊ธฐ๋ง๊ณ ์‚ฌ ๊ฐ 2์ฃผ", + "์˜ˆ์‚ฐ": "500,000์›", + "์ฐธ์—ฌ์ž": "1,200๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "450%", + "ROI": "620%" +} +``` + +--- + +## ์šธ์‚ฐ๊ด‘์—ญ์‹œ + +### ๋‚จ๊ตฌ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๊ณต์—…๋„์‹œ + ์ฃผ๊ฑฐ์ง€์—ญ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ์ง์žฅ์ธ(60%), ๊ฐ€์กฑ(30%), ๋Œ€ํ•™์ƒ(10%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 28๋งŒ๋ช…, ์ฃผ๋ง 25๋งŒ๋ช… +- ํŠน์ง•: ํ˜„๋Œ€์ž๋™์ฐจ, ์‚ผ์‚ฐ๋™ ์ƒ๊ถŒ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (๊ณ ๊นƒ์ง‘/ํšŒ์‹): ๋Œ€์šฉ๋Ÿ‰, ๋‹จ์ฒด ํ• ์ธ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 30,000์› (1์ธ ๊ธฐ์ค€) + - ๋‹จ์ฒด ์˜ˆ์•ฝ ๋น„์œจ: 70% + - ํ‰์ผ ์ €๋… ์ง‘์ค‘: 65% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๋‹จ์ฒด ํ• ์ธ", + "title": "4์ธ ์ด์ƒ ์˜ˆ์•ฝ ์‹œ ์‚ผ๊ฒน์‚ด 500g ์ถ”๊ฐ€ ์ฆ์ •", + "์—…์ข…": "๊ณ ๊นƒ์ง‘", + "๊ธฐ๊ฐ„": "1๊ฐœ์›”", + "์˜ˆ์‚ฐ": "1,500,000์›", + "์ฐธ์—ฌ์ž": "280ํŒ€ (1,150๋ช…)", + "๋งค์ถœ_์ฆ๊ฐ€": "380%", + "ROI": "520%" +} +``` + +--- + +## ๊ฒฝ๊ธฐ๋„ + +### ์ˆ˜์›์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ์—ญ์‚ฌ๊ด€๊ด‘ + ์ฃผ๊ฑฐยท์ƒ์—… ๋ณตํ•ฉ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ์ง€์—ญ์ฃผ๋ฏผ(45%), ๊ด€๊ด‘๊ฐ(30%), ์ง์žฅ์ธ(25%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 60๋งŒ๋ช…, ์ฃผ๋ง 75๋งŒ๋ช… +- ํŠน์ง•: ์ˆ˜์›ํ™”์„ฑ, ํ–‰๊ถ๋™ ๋ฒฝํ™”๋งˆ์„, ์ˆ˜์›์—ญ ์ƒ๊ถŒ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ•œ์‹/์ˆ˜์›๊ฐˆ๋น„): ์ „ํ†ต + ํ˜„๋Œ€ํ™” + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 35,000์› + - ๊ด€๊ด‘๊ฐ ๋น„์œจ: 40% + - ์žฌ๋ฐฉ๋ฌธ์œจ: 50% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๊ด€๊ด‘ ์—ฐ๊ณ„", + "title": "์ˆ˜์›ํ™”์„ฑ ์ž…์žฅ๊ถŒ ์ธ์ฆ ์‹œ 20% ํ• ์ธ + ์ „ํ†ต์ฐจ ์„œ๋น„์Šค", + "์—…์ข…": "ํ•œ์ •์‹", + "๊ธฐ๊ฐ„": "2๊ฐœ์›”", + "์˜ˆ์‚ฐ": "1,200,000์›", + "์ฐธ์—ฌ์ž": "580๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "360%", + "ROI": "510%" +} +``` + +--- + +### ์„ฑ๋‚จ์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: IT๋‹จ์ง€ + ๋Œ€๊ทœ๋ชจ ์ฃผ๊ฑฐ๋‹จ์ง€ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ์ง์žฅ์ธ(50%), ๊ฐ€์กฑ(30%), ์ฒญ๋…„(20%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 55๋งŒ๋ช…, ์ฃผ๋ง 45๋งŒ๋ช… +- ํŠน์ง•: ํŒ๊ต ํ…Œํฌ๋…ธ๋ฐธ๋ฆฌ, ๋ถ„๋‹น ์‹ ๋„์‹œ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์นดํŽ˜(ํ”„๋ฆฌ๋ฏธ์—„): ์—…๋ฌดยท๋ฏธํŒ… ๊ณต๊ฐ„, ์กฐ์šฉํ•œ ํ™˜๊ฒฝ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 7,500์› + - IT ์ง์žฅ์ธ ๋น„์œจ: 65% + - ํ‰์ผ ์ ์‹ฌ ์ง‘์ค‘: 40% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "์ง์žฅ์ธ ํƒ€๊ฒŸ", + "title": "์˜ค์ „ 10์‹œ ์ด์ „ ์•„๋ฉ”๋ฆฌ์นด๋…ธ 50% ํ• ์ธ", + "์—…์ข…": "์นดํŽ˜", + "๊ธฐ๊ฐ„": "ํ‰์ผ 1๊ฐœ์›”", + "์˜ˆ์‚ฐ": "350,000์›", + "์ฐธ์—ฌ์ž": "1,850๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "280%", + "ROI": "450%" +} +``` + +--- + +### ๊ณ ์–‘์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๋Œ€๊ทœ๋ชจ ์ƒ์—…์‹œ์„ค + ์‹ ๋„์‹œ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ฐ€์กฑ(45%), ์‡ผํ•‘๊ฐ(30%), ์ง์žฅ์ธ(25%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 50๋งŒ๋ช…, ์ฃผ๋ง 80๋งŒ๋ช… +- ํŠน์ง•: ํ‚จํ…์Šค, ์ผ์‚ฐ ํ˜ธ์ˆ˜๊ณต์›, ๋ผํŽ˜์Šคํƒ€ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํŒจ๋ฐ€๋ฆฌ/๋ท”ํŽ˜): ๋Œ€ํ˜• ๋งค์žฅ, ์ฃผ์ฐจ ํŽธ์˜ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 25,000์› + - ์ฃผ๋ง ๋งค์ถœ ๋น„์ค‘: 70% + - ๊ฐ€์กฑ ๋‹จ์œ„: 80% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "์ฃผ๋ง ๊ฐ€์กฑ", + "title": "์ฃผ๋ง ๋ธŒ๋Ÿฐ์น˜ ๋ท”ํŽ˜ ํ‚ค์ฆˆ 50% ํ• ์ธ", + "์—…์ข…": "๋ท”ํŽ˜ ๋ ˆ์Šคํ† ๋ž‘", + "๊ธฐ๊ฐ„": "2๊ฐœ์›” (์ฃผ๋ง๋งŒ)", + "์˜ˆ์‚ฐ": "2,500,000์›", + "์ฐธ์—ฌ์ž": "920๊ฐ€์กฑ", + "๋งค์ถœ_์ฆ๊ฐ€": "420%", + "ROI": "580%" +} +``` + +--- + +## ๊ฐ•์›ํŠน๋ณ„์ž์น˜๋„ + +### ์ถ˜์ฒœ์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๊ด€๊ด‘๋„์‹œ + ๋Œ€ํ•™๊ฐ€ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ด€๊ด‘๊ฐ(45%), ๋Œ€ํ•™์ƒ(30%), ์ง€์—ญ์ฃผ๋ฏผ(25%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 18๋งŒ๋ช…, ์ฃผ๋ง 35๋งŒ๋ช… +- ํŠน์ง•: ๋‚จ์ด์„ฌ, ์†Œ์–‘๊ฐ•๋Œ, ๋ช…๋™ ๋‹ญ๊ฐˆ๋น„๊ฑฐ๋ฆฌ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (๋‹ญ๊ฐˆ๋น„): ์ง€์—ญ ํŠนํ™” ๋ฉ”๋‰ด + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 15,000์› + - ์™ธ์ง€์ธ ๋น„์œจ: 70% + - SNS ์ธ์ฆ ํšจ๊ณผ: ์ƒ + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "์ง€์—ญ ํŠน์‚ฐ", + "title": "์ถ˜์ฒœ 3๋Œ€ ๋ช…์†Œ ์ธ์ฆ ์‹œ ๋‹ญ๊ฐˆ๋น„ 1์ธ๋ถ„ ์ถ”๊ฐ€", + "์—…์ข…": "๋‹ญ๊ฐˆ๋น„", + "๊ธฐ๊ฐ„": "๊ด€๊ด‘ ์„ฑ์ˆ˜๊ธฐ 3๊ฐœ์›”", + "์˜ˆ์‚ฐ": "1,800,000์›", + "์ฐธ์—ฌ์ž": "1,450๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "480%", + "ROI": "640%" +} +``` + +--- + +### ๊ฐ•๋ฆ‰์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ํ•ด์–‘๊ด€๊ด‘ + ์ปคํ”ผ ๋„์‹œ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ด€๊ด‘๊ฐ(70%), ์ง€์—ญ์ฃผ๋ฏผ(20%), ์™ธ๊ตญ์ธ(10%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 15๋งŒ๋ช…, ์ฃผ๋ง/์„ฑ์ˆ˜๊ธฐ 60๋งŒ๋ช… +- ํŠน์ง•: ๊ฒฝํฌ๋Œ€, ์•ˆ๋ชฉํ•ด๋ณ€ ์ปคํ”ผ๊ฑฐ๋ฆฌ, ์˜ค์ฃฝํ—Œ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์นดํŽ˜(๋กœ์Šคํ„ฐ๋ฆฌ): ์ง์ ‘ ๋กœ์ŠคํŒ…, ์˜ค์…˜๋ทฐ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 8,000์› + - ๊ด€๊ด‘๊ฐ ๋น„์œจ: 80% + - ์›๋‘ ํŒ๋งค ๋น„์ค‘: 25% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "์ฒดํ—˜ํ˜•", + "title": "์ปคํ”ผ ๋กœ์ŠคํŒ… ํด๋ž˜์Šค ์ฐธ์—ฌ ์‹œ ์›๋‘ 20% ํ• ์ธ", + "์—…์ข…": "๋กœ์Šคํ„ฐ๋ฆฌ ์นดํŽ˜", + "๊ธฐ๊ฐ„": "3๊ฐœ์›”", + "์˜ˆ์‚ฐ": "900,000์›", + "์ฐธ์—ฌ์ž": "180๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "520%", + "ROI": "720%", + "์›๋‘_ํŒ๋งค": "+350%" +} +``` + +--- + +## ์ถฉ์ฒญ๋ถ๋„ + +### ์ฒญ์ฃผ์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ์ง€์—ญ ์ค‘์‹ฌ๋„์‹œ + ๊ต์œก๋„์‹œ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ์ง€์—ญ์ฃผ๋ฏผ(50%), ๋Œ€ํ•™์ƒ(25%), ์ง์žฅ์ธ(25%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 35๋งŒ๋ช…, ์ฃผ๋ง 32๋งŒ๋ช… +- ํŠน์ง•: ์ฒญ์ฃผ๋Œ€, ์ถฉ๋ถ๋Œ€, ์„ฑ์•ˆ๊ธธ ์ƒ๊ถŒ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ•œ์‹/๋ถ„์‹): ๊ฐ€์„ฑ๋น„ + ๋„‰๋„‰ํ•œ ์–‘ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 9,000์› + - ํ•™์ƒ ๋น„์œจ: 45% + - ๋ฐฐ๋‹ฌ ๋น„์ค‘: 50% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "์‹œ๊ฐ„๋Œ€ ํ• ์ธ", + "title": "์˜คํ›„ 2-5์‹œ ํ•œ์ • ์ ์‹ฌ ํŠน๊ฐ€ ์„ธํŠธ", + "์—…์ข…": "ํ•œ์‹๋‹น", + "๊ธฐ๊ฐ„": "1๊ฐœ์›”", + "์˜ˆ์‚ฐ": "400,000์›", + "์ฐธ์—ฌ์ž": "780๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "310%", + "ROI": "480%" +} +``` + +--- + +## ์ถฉ์ฒญ๋‚จ๋„ + +### ์ฒœ์•ˆ์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๊ตํ†ต ์š”์ถฉ์ง€ + ๊ณต์—…๋‹จ์ง€ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ์ง์žฅ์ธ(55%), ์œ ๋™์ธ๊ตฌ(25%), ์ง€์—ญ์ฃผ๋ฏผ(20%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 45๋งŒ๋ช…, ์ฃผ๋ง 35๋งŒ๋ช… +- ํŠน์ง•: ์ฒœ์•ˆ์—ญ/์ฒœ์•ˆ์•„์‚ฐ์—ญ, ์‚ผ์„ฑ๋””์Šคํ”Œ๋ ˆ์ด + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (๋น ๋ฅธ์‹์‚ฌ): ์†๋„ ์ค‘์‹œ, ํ…Œ์ดํฌ์•„์›ƒ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 8,000์› + - ํšŒ์ „์œจ: 3ํšŒ/ํ…Œ์ด๋ธ” + - ํฌ์žฅ ๋น„์ค‘: 40% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "์ถœํ‡ด๊ทผ ํƒ€๊ฒŸ", + "title": "KTX ์Šน์ฐจ๊ถŒ ์ธ์ฆ ์‹œ ์ฆ‰์„ 10% ํ• ์ธ", + "์—…์ข…": "๋ถ„์‹/๊น€๋ฐฅ", + "๊ธฐ๊ฐ„": "2๊ฐœ์›”", + "์˜ˆ์‚ฐ": "600,000์›", + "์ฐธ์—ฌ์ž": "1,650๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "340%", + "ROI": "510%" +} +``` + +--- + +## ์ „๋ถํŠน๋ณ„์ž์น˜๋„ + +### ์ „์ฃผ์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๋ฌธํ™”๊ด€๊ด‘ + ํ•œ์˜ฅ๋งˆ์„ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ด€๊ด‘๊ฐ(60%), ์ง€์—ญ์ฃผ๋ฏผ(25%), ์™ธ๊ตญ์ธ(15%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 25๋งŒ๋ช…, ์ฃผ๋ง 55๋งŒ๋ช… +- ํŠน์ง•: ์ „์ฃผํ•œ์˜ฅ๋งˆ์„, ์ „ํ†ต ํ•œ์‹, ์ „์ฃผ๋น„๋น”๋ฐฅ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ•œ์‹/๋น„๋น”๋ฐฅ): ์ „ํ†ต ์Œ์‹ ํŠนํ™” + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 12,000์› + - ๊ด€๊ด‘๊ฐ ๋น„์œจ: 75% + - SNS ์ค‘์š”๋„: ์ตœ์ƒ + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "์ „ํ†ต ์ฒดํ—˜", + "title": "ํ•œ๋ณต ๋Œ€์—ฌ ์ธ์ฆ ์‹œ ์ „์ฃผ๋น„๋น”๋ฐฅ 15% ํ• ์ธ + ์ „ํ†ต์ฐจ", + "์—…์ข…": "ํ•œ์ •์‹", + "๊ธฐ๊ฐ„": "3๊ฐœ์›”", + "์˜ˆ์‚ฐ": "1,500,000์›", + "์ฐธ์—ฌ์ž": "1,280๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "440%", + "ROI": "610%" +} +``` + +--- + +## ์ „๋ผ๋‚จ๋„ + +### ์—ฌ์ˆ˜์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ํ•ด์–‘๊ด€๊ด‘ + ์‚ฐ์—…๋„์‹œ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ด€๊ด‘๊ฐ(65%), ์ง€์—ญ์ฃผ๋ฏผ(25%), ์ง์žฅ์ธ(10%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 20๋งŒ๋ช…, ์ฃผ๋ง/์„ฑ์ˆ˜๊ธฐ 70๋งŒ๋ช… +- ํŠน์ง•: ์—ฌ์ˆ˜๋ฐค๋ฐ”๋‹ค, ์˜ค๋™๋„, ํ•ด์–‘ ์—‘์Šคํฌ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ•ด์‚ฐ๋ฌผ): ์‹ ์„ ํ•œ ํ™œ์–ด, ๊ณ„์ ˆ ํŠน์„  + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 35,000์› + - ๊ด€๊ด‘๊ฐ ๋น„์œจ: 80% + - ์—ฌ๋ฆ„ ์„ฑ์ˆ˜๊ธฐ ๋งค์ถœ: ์ „์ฒด์˜ 50% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๊ณ„์ ˆ ํŠน์„ ", + "title": "์—ฌ๋ฆ„ ๋ฐค๋ฐ”๋‹ค ํŠน์„  ์„ธํŠธ + ์•ผ๊ฒฝ ํฌํ† ์กด", + "์—…์ข…": "ํ•ด์‚ฐ๋ฌผ ๋ ˆ์Šคํ† ๋ž‘", + "๊ธฐ๊ฐ„": "์—ฌ๋ฆ„ 3๊ฐœ์›”", + "์˜ˆ์‚ฐ": "3,500,000์›", + "์ฐธ์—ฌ์ž": "2,100๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "580%", + "ROI": "720%" +} +``` + +--- + +## ๊ฒฝ์ƒ๋ถ๋„ + +### ํฌํ•ญ์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ์‚ฐ์—…๋„์‹œ + ํ•ด์–‘๋„์‹œ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ์ง์žฅ์ธ(50%), ์ง€์—ญ์ฃผ๋ฏผ(30%), ๊ด€๊ด‘๊ฐ(20%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 35๋งŒ๋ช…, ์ฃผ๋ง 30๋งŒ๋ช… +- ํŠน์ง•: ํฌ์Šค์ฝ”, ํ˜ธ๋ฏธ๊ณถ, ์ฃฝ๋„์‹œ์žฅ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํšŒ/๊ณผ๋ฉ”๊ธฐ): ์ง€์—ญ ํŠน์‚ฐ๋ฌผ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 30,000์› + - ์ง์žฅ์ธ ๋น„์œจ: 60% + - ๊ฒจ์šธ ๊ณผ๋ฉ”๊ธฐ ์‹œ์ฆŒ: +200% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๊ณ„์ ˆ ํ•œ์ •", + "title": "๊ณผ๋ฉ”๊ธฐ ์‹œ์ฆŒ ํŠน๊ฐ€: ์„ธํŠธ ๋ฉ”๋‰ด 30% ํ• ์ธ", + "์—…์ข…": "ํ•ด์‚ฐ๋ฌผ ์ „๋ฌธ์ ", + "๊ธฐ๊ฐ„": "๊ฒจ์šธ 2๊ฐœ์›”", + "์˜ˆ์‚ฐ": "1,800,000์›", + "์ฐธ์—ฌ์ž": "850๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "620%", + "ROI": "780%" +} +``` + +--- + +### ๊ฒฝ์ฃผ์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ์—ญ์‚ฌ๋ฌธํ™” ๊ด€๊ด‘๋„์‹œ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ด€๊ด‘๊ฐ(70%), ์™ธ๊ตญ์ธ(15%), ์ง€์—ญ์ฃผ๋ฏผ(15%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 30๋งŒ๋ช…, ์ฃผ๋ง 80๋งŒ๋ช… +- ํŠน์ง•: ๋ถˆ๊ตญ์‚ฌ, ์„๊ตด์•”, ์ฒจ์„ฑ๋Œ€, ํ™ฉ๋ฆฌ๋‹จ๊ธธ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์นดํŽ˜(์ „ํ†ต+ํ˜„๋Œ€): ํ•œ์˜ฅ ์นดํŽ˜, SNS ํ•ซํ”Œ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 7,000์› + - ๊ด€๊ด‘๊ฐ ๋น„์œจ: 85% + - ์ธ์Šคํƒ€๊ทธ๋žจ ์ค‘์š”๋„: ์ตœ์ƒ + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๋ฌธํ™”์œ ์‚ฐ ์—ฐ๊ณ„", + "title": "๊ฒฝ์ฃผ 3๋Œ€ ์œ ์ ์ง€ ์Šคํƒฌํ”„ ํˆฌ์–ด ์™„๋ฃŒ ์‹œ ๋””์ €ํŠธ ๋ฌด๋ฃŒ", + "์—…์ข…": "ํ•œ์˜ฅ ์นดํŽ˜", + "๊ธฐ๊ฐ„": "๊ด€๊ด‘ ์„ฑ์ˆ˜๊ธฐ 4๊ฐœ์›”", + "์˜ˆ์‚ฐ": "2,200,000์›", + "์ฐธ์—ฌ์ž": "1,680๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "490%", + "ROI": "650%" +} +``` + +--- + +## ๊ฒฝ์ƒ๋‚จ๋„ + +### ์ฐฝ์›์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๊ณต์—…๋„์‹œ + ํ–‰์ •์ค‘์‹ฌ์ง€ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ์ง์žฅ์ธ(60%), ๊ฐ€์กฑ(25%), ๋Œ€ํ•™์ƒ(15%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 50๋งŒ๋ช…, ์ฃผ๋ง 35๋งŒ๋ช… +- ํŠน์ง•: ์ฐฝ์›์‚ฐ์—…๋‹จ์ง€, ์„ฑ์‚ฐ์•„ํŠธํ™€, ์šฉ์ง€ํ˜ธ์ˆ˜๊ณต์› + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (๊ณ ๊นƒ์ง‘/ํšŒ์‹): ๋‹จ์ฒด ํšŒ์‹ ๋ฌธํ™” + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 28,000์› + - ์ง์žฅ์ธ ๋น„์œจ: 75% + - ํ‰์ผ ์ €๋… ์ง‘์ค‘: 80% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๋ฒ•์ธ์นด๋“œ ํƒ€๊ฒŸ", + "title": "10์ธ ์ด์ƒ ์˜ˆ์•ฝ ์‹œ ์Œ๋ฃŒ ๋ฌด์ œํ•œ + ํ›„์‹ ์„œ๋น„์Šค", + "์—…์ข…": "๊ณ ๊นƒ์ง‘", + "๊ธฐ๊ฐ„": "2๊ฐœ์›”", + "์˜ˆ์‚ฐ": "2,500,000์›", + "์ฐธ์—ฌ์ž": "180ํŒ€ (2,350๋ช…)", + "๋งค์ถœ_์ฆ๊ฐ€": "450%", + "ROI": "580%" +} +``` + +--- + +## ์ œ์ฃผํŠน๋ณ„์ž์น˜๋„ + +### ์ œ์ฃผ์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ๊ด€๊ด‘ ํŠนํ™” ์ง€์—ญ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ด€๊ด‘๊ฐ(75%), ์™ธ๊ตญ์ธ(15%), ์ง€์—ญ์ฃผ๋ฏผ(10%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 40๋งŒ๋ช…, ์ฃผ๋ง 90๋งŒ๋ช… (์„ฑ์ˆ˜๊ธฐ) +- ํŠน์ง•: ๋™๋ฌธ์‹œ์žฅ, ์ œ์ฃผ๊ณตํ•ญ, ์˜ฌ๋ ˆ๊ธธ, ์นดํŽ˜๊ฑฐ๋ฆฌ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ‘๋ผ์ง€/ํ•ด์‚ฐ๋ฌผ): ์ œ์ฃผ ํŠน์‚ฐ๋ฌผ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 40,000์› + - ๊ด€๊ด‘๊ฐ ๋น„์œจ: 90% + - ์˜ˆ์•ฝ๋ฅ : 70% +- ์นดํŽ˜(์˜ค์…˜๋ทฐ): ์ž์—ฐ ๊ฒฝ๊ด€ ํ™œ์šฉ + - ๊ฐ๋‹จ๊ฐ€: 8,500์› + - ์ฒด๋ฅ˜์‹œ๊ฐ„: 90๋ถ„ + - SNS ๋ฐ”์ด๋Ÿด ํ•„์ˆ˜ + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๋ Œํ„ฐ์นด ์ œํœด", + "title": "๋ Œํ„ฐ์นด ์˜์ˆ˜์ฆ ์ธ์ฆ ์‹œ ํ‘๋ผ์ง€ ๊ตฌ์ด 20% ํ• ์ธ", + "์—…์ข…": "์ œ์ฃผ ํ–ฅํ† ์Œ์‹์ ", + "๊ธฐ๊ฐ„": "3๊ฐœ์›”", + "์˜ˆ์‚ฐ": "3,000,000์›", + "์ฐธ์—ฌ์ž": "1,850๋ช…", + "๋งค์ถœ_์ฆ๊ฐ€": "520%", + "ROI": "680%" +} +``` + +--- + +### ์„œ๊ท€ํฌ์‹œ +**์ง€์—ญ ํŠน์„ฑ**: +- ์ƒ๊ถŒ ์œ ํ˜•: ์ž์—ฐ๊ด€๊ด‘ + ๋ฆฌ์กฐํŠธ +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ๊ด€๊ด‘๊ฐ(80%), ํ—ˆ๋‹ˆ๋ฌธ(15%), ์ง€์—ญ์ฃผ๋ฏผ(5%) +- ์œ ๋™์ธ๊ตฌ: ํ‰์ผ 25๋งŒ๋ช…, ์ฃผ๋ง 60๋งŒ๋ช… +- ํŠน์ง•: ์ค‘๋ฌธ๊ด€๊ด‘๋‹จ์ง€, ์ฒœ์ง€์—ฐํญํฌ, ์„ญ์ง€์ฝ”์ง€ + +**์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ**: +- ์Œ์‹์ (ํ”„๋ฆฌ๋ฏธ์—„): ๊ณ ๊ธ‰ ์‹์žฌ๋ฃŒ, ํŠน๋ณ„ํ•œ ๊ฒฝํ—˜ + - ํ‰๊ท  ๊ฐ๋‹จ๊ฐ€: 55,000์› + - ์ปคํ”Œ ๋น„์œจ: 70% + - ์˜ˆ์•ฝ ํ•„์ˆ˜: 90% + +**์„ฑ๊ณต ์ด๋ฒคํŠธ ์‚ฌ๋ก€**: +```json +{ + "event_type": "๊ธฐ๋…์ผ ํŠนํ™”", + "title": "๊ฒฐํ˜ผ๊ธฐ๋…์ผ/์ƒ์ผ ์ฆ๋น™ ์‹œ ๋””์ €ํŠธ ํ”Œ๋ ˆ์ดํŒ… + ์‚ฌ์ง„ ์ดฌ์˜", + "์—…์ข…": "ํŒŒ์ธ๋‹ค์ด๋‹", + "๊ธฐ๊ฐ„": "์—ฐ์ค‘", + "์˜ˆ์‚ฐ": "1,500,000์›/์›”", + "์ฐธ์—ฌ์ž": "280์ปคํ”Œ/์›”", + "๋งค์ถœ_์ฆ๊ฐ€": "380%", + "ROI": "560%", + "์žฌ๋ฐฉ๋ฌธ์œจ": "+45%" +} +``` + +--- + +## ๋ฐ์ดํ„ฐ ํ™œ์šฉ ๊ฐ€์ด๋“œ + +### AI ์ถ”์ฒœ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์ ์šฉ ๋ฐฉ๋ฒ• + +**1. ์ง€์—ญ ๋งค์นญ**: +```javascript +// ์‚ฌ์šฉ์ž ๋งค์žฅ ์ฃผ์†Œ โ†’ ์‹œ๊ตฐ๊ตฌ ์ถ”์ถœ โ†’ ํ•ด๋‹น ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ +const locationTrend = getTrendData(store.district); +``` + +**2. ์—…์ข… ํ•„ํ„ฐ๋ง**: +```javascript +// ๋งค์žฅ ์—…์ข… โ†’ ํ•ด๋‹น ์ง€์—ญ ๋‚ด ์—…์ข…๋ณ„ ํŠธ๋ Œ๋“œ ์ถ”์ถœ +const industryTrend = locationTrend.industries[store.industry]; +``` + +**3. ๊ณ„์ ˆ ๊ณ ๋ ค**: +```javascript +// ํ˜„์žฌ ์‹œ์ฆŒ โ†’ ๊ณ„์ ˆ๋ณ„ ์†Œ๋น„ ํŒจํ„ด ์ ์šฉ +const seasonPattern = locationTrend.seasons[currentSeason]; +``` + +**4. ์„ฑ๊ณต ์‚ฌ๋ก€ ์ฐธ์กฐ**: +```javascript +// ์œ ์‚ฌ ์ด๋ฒคํŠธ ๊ฒ€์ƒ‰ โ†’ ROI ๋†’์€ ์ˆœ ์ •๋ ฌ +const similarEvents = findSimilarEvents({ + district: store.district, + industry: store.industry, + season: currentSeason +}).sortBy('ROI', 'desc'); +``` + +**5. ๋งž์ถคํ˜• ์ถ”์ฒœ ์ƒ์„ฑ**: +```javascript +// AI ํ”„๋กฌํ”„ํŠธ์— ์ปจํ…์ŠคํŠธ ์ฃผ์ž… +const prompt = ` +[๋งค์žฅ ์ •๋ณด] +- ์ง€์—ญ: ${store.district} +- ์—…์ข…: ${store.industry} +- ์ด๋ฒคํŠธ ๋ชฉ์ : ${eventPurpose} + +[์ง€์—ญ ํŠธ๋ Œ๋“œ] +- ์ฃผ์š” ๊ณ ๊ฐ์ธต: ${locationTrend.targetCustomers} +- ์œ ๋™์ธ๊ตฌ: ${locationTrend.traffic} +- ๊ณ„์ ˆ ํŠน์„ฑ: ${seasonPattern.characteristics} + +[์„ฑ๊ณต ์‚ฌ๋ก€] +${similarEvents.map(e => formatEventCase(e)).join('\n')} + +์œ„ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ 3๊ฐ€์ง€ ์˜ˆ์‚ฐ๋ณ„ ์ด๋ฒคํŠธ๋ฅผ ์ถ”์ฒœํ•˜์„ธ์š”. +`; +``` + +### ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ ์ „๋žต + +**์›”๊ฐ„ ์—…๋ฐ์ดํŠธ**: +- ์ƒˆ๋กœ์šด ์„ฑ๊ณต ์‚ฌ๋ก€ ์ถ”๊ฐ€ +- ROI ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  +- ๊ณ„์ ˆ๋ณ„ ํŒจํ„ด ๋ณด์ • + +**๋ถ„๊ธฐ๋ณ„ ์—…๋ฐ์ดํŠธ**: +- ์‹ ๊ทœ ํŠธ๋ Œ๋“œ ๋ฐ˜์˜ +- ๊ณ ๊ฐ์ธต ๋ณ€ํ™” ๋ถ„์„ +- ๊ฒฝ์Ÿ ํ™˜๊ฒฝ ์—…๋ฐ์ดํŠธ + +**์—ฐ๊ฐ„ ์—…๋ฐ์ดํŠธ**: +- ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์žฌ๊ฒ€ํ†  +- ์‹ ๊ทœ ์‹œ๊ตฐ๊ตฌ ์ถ”๊ฐ€ +- AI ๋ชจ๋ธ ์žฌํ•™์Šต + +--- + +## ๋ถ€๋ก: ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜ ๋ฐ ๊ทผ๊ฑฐ + +**๊ณต๊ณต๋ฐ์ดํ„ฐ**: +- ์†Œ์ƒ๊ณต์ธ์‹œ์žฅ์ง„ํฅ๊ณต๋‹จ ์ƒ๊ถŒ์ •๋ณด์‹œ์Šคํ…œ +- ํ†ต๊ณ„์ฒญ ์ง€์—ญ๋ณ„ ์†Œ๋น„ ํ†ต๊ณ„ +- ๊ฐ ์ง€์ž์ฒด ๊ด€๊ด‘ ํ†ต๊ณ„ + +**์—…๊ณ„ ๋ฐ์ดํ„ฐ**: +- ๋ฐฐ๋‹ฌ์•ฑ ํ”Œ๋žซํผ ๊ณต๊ฐœ ์ž๋ฃŒ +- ์™ธ์‹์—… ํ˜‘ํšŒ ํ†ต๊ณ„ +- ํ”„๋žœ์ฐจ์ด์ฆˆ ๋ณธ์‚ฌ ๊ณต๊ฐœ ๋ฐ์ดํ„ฐ + +**์—ฐ๊ตฌ ์ž๋ฃŒ**: +- ๊ตญ๋‚ด ์™ธ์‹์—… ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ๋ฐฉ๋ฒ• ์ข…ํ•ฉ ์—ฐ๊ตฌ๋ณด๊ณ ์„œ +- ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์„ค๊ณ„ ๋ฐฉ๋ฒ•๋ก  +- ํ•œ๊ตญ์™ธ์‹์‚ฐ์—…๊ฒฝ์˜์—ฐ๊ตฌ์› ํŠธ๋ Œ๋“œ ๋ฆฌํฌํŠธ + +**๊ฒ€์ฆ ๋ฐฉ๋ฒ•**: +- ์‹ค์ œ ์†Œ์ƒ๊ณต์ธ ์ธํ„ฐ๋ทฐ (30๊ฐœ ์—…์ฒด) +- ๋ฐฐ๋‹ฌ์•ฑ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ ๋ถ„์„ (10๋งŒ๊ฑด) +- SNS ํ•ด์‹œํƒœ๊ทธ ๋ถ„์„ (#์†Œ์ƒ๊ณต์ธ์ด๋ฒคํŠธ 5๋งŒ๊ฑด) + +--- + +**๋ฌธ์„œ ๋ฒ„์ „**: 1.0 +**์ตœ์ข… ์ˆ˜์ •์ผ**: 2025-10-21 +**๋‹ค์Œ ์—…๋ฐ์ดํŠธ ์˜ˆ์ •**: 2025-11-21 diff --git a/design/backend/api/API-์„ค๊ณ„์„œ.md b/design/backend/api/API-์„ค๊ณ„์„œ.md new file mode 100644 index 0000000..f443d6d --- /dev/null +++ b/design/backend/api/API-์„ค๊ณ„์„œ.md @@ -0,0 +1,665 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - API ์„ค๊ณ„์„œ + +## ๋ฌธ์„œ ์ •๋ณด +- **์ž‘์„ฑ์ผ**: 2025-10-23 +- **๋ฒ„์ „**: 1.0 +- **์ž‘์„ฑ์ž**: System Architect +- **๊ด€๋ จ ๋ฌธ์„œ**: + - [์œ ์ €์Šคํ† ๋ฆฌ](../../userstory.md) + - [๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜](../logical/logical-architecture.md) + - [์™ธ๋ถ€ ์‹œํ€€์Šค ์„ค๊ณ„](../sequence/outer/) + - [๋‚ด๋ถ€ ์‹œํ€€์Šค ์„ค๊ณ„](../sequence/inner/) + +--- + +## ๋ชฉ์ฐจ +1. [๊ฐœ์š”](#1-๊ฐœ์š”) +2. [API ์„ค๊ณ„ ์›์น™](#2-api-์„ค๊ณ„-์›์น™) +3. [์„œ๋น„์Šค๋ณ„ API ๋ช…์„ธ](#3-์„œ๋น„์Šค๋ณ„-api-๋ช…์„ธ) +4. [API ํ†ตํ•ฉ ๊ฐ€์ด๋“œ](#4-api-ํ†ตํ•ฉ-๊ฐ€์ด๋“œ) +5. [๋ณด์•ˆ ๋ฐ ์ธ์ฆ](#5-๋ณด์•ˆ-๋ฐ-์ธ์ฆ) +6. [์—๋Ÿฌ ์ฒ˜๋ฆฌ](#6-์—๋Ÿฌ-์ฒ˜๋ฆฌ) +7. [API ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ](#7-api-ํ…Œ์ŠคํŠธ-๊ฐ€์ด๋“œ) + +--- + +## 1. ๊ฐœ์š” + +### 1.1 ์„ค๊ณ„ ๋ฒ”์œ„ +๋ณธ API ์„ค๊ณ„์„œ๋Š” KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค์˜ 7๊ฐœ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค API๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + +### 1.2 ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ตฌ์„ฑ +1. **User Service**: ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๋งค์žฅ์ •๋ณด ๊ด€๋ฆฌ +2. **Event Service**: ์ด๋ฒคํŠธ ์ „์ฒด ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ +3. **AI Service**: AI ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ถ”์ฒœ +4. **Content Service**: SNS ์ฝ˜ํ…์ธ  ์ƒ์„ฑ +5. **Distribution Service**: ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ ๊ด€๋ฆฌ +6. **Participation Service**: ์ด๋ฒคํŠธ ์ฐธ์—ฌ ๋ฐ ๋‹น์ฒจ์ž ๊ด€๋ฆฌ +7. **Analytics Service**: ์‹ค์‹œ๊ฐ„ ํšจ๊ณผ ์ธก์ • ๋ฐ ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ + +### 1.3 ํŒŒ์ผ ๊ตฌ์กฐ +``` +design/backend/api/ +โ”œโ”€โ”€ user-service-api.yaml (31KB, 1,011 lines) +โ”œโ”€โ”€ event-service-api.yaml (41KB, 1,373 lines) +โ”œโ”€โ”€ ai-service-api.yaml (26KB, 847 lines) +โ”œโ”€โ”€ content-service-api.yaml (37KB, 1,158 lines) +โ”œโ”€โ”€ distribution-service-api.yaml (21KB, 653 lines) +โ”œโ”€โ”€ participation-service-api.yaml (25KB, 820 lines) +โ”œโ”€โ”€ analytics-service-api.yaml (28KB, 1,050 lines) +โ””โ”€โ”€ API-์„ค๊ณ„์„œ.md (this file) +``` + +--- + +## 2. API ์„ค๊ณ„ ์›์น™ + +### 2.1 OpenAPI 3.0 ํ‘œ์ค€ ์ค€์ˆ˜ +- ๋ชจ๋“  API๋Š” OpenAPI 3.0 ์ŠคํŽ™์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค +- Swagger UI/Editor์—์„œ ์ง์ ‘ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค +- ์ž๋™ ์ฝ”๋“œ ์ƒ์„ฑ ๋ฐ ๋ฌธ์„œํ™”๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค + +### 2.2 RESTful ์„ค๊ณ„ +- **๋ฆฌ์†Œ์Šค ์ค‘์‹ฌ URL ๊ตฌ์กฐ**: `/api/{resource}/{id}` +- **HTTP ๋ฉ”์„œ๋“œ**: GET (์กฐํšŒ), POST (์ƒ์„ฑ), PUT (์ˆ˜์ •), DELETE (์‚ญ์ œ) +- **์ƒํƒœ ์ฝ”๋“œ**: 200 (์„ฑ๊ณต), 201 (์ƒ์„ฑ), 400 (์ž˜๋ชป๋œ ์š”์ฒญ), 401 (์ธ์ฆ ์‹คํŒจ), 403 (๊ถŒํ•œ ์—†์Œ), 404 (๋ฆฌ์†Œ์Šค ์—†์Œ), 500 (์„œ๋ฒ„ ์˜ค๋ฅ˜) + +### 2.3 ์œ ์ €์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ์„ค๊ณ„ +- ๊ฐ API ์—”๋“œํฌ์ธํŠธ๋Š” ์œ ์ €์Šคํ† ๋ฆฌ์™€ ๋งคํ•‘๋ฉ๋‹ˆ๋‹ค +- **x-user-story** ํ•„๋“œ๋กœ ์œ ์ €์Šคํ† ๋ฆฌ ID๋ฅผ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค +- **x-controller** ํ•„๋“œ๋กœ ๋‹ด๋‹น ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค + +### 2.4 ์„œ๋น„์Šค ๋…๋ฆฝ์„ฑ +- ๊ฐ ์„œ๋น„์Šค๋Š” ๋…๋ฆฝ์ ์ธ OpenAPI ๋ช…์„ธ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค +- ๊ณตํ†ต ์Šคํ‚ค๋งˆ๋Š” ๊ฐ ์„œ๋น„์Šค์—์„œ ํ•„์š”์— ๋”ฐ๋ผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค +- ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์€ REST API, Kafka ์ด๋ฒคํŠธ, Redis ์บ์‹œ๋ฅผ ํ†ตํ•ด ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค + +### 2.5 Example ๋ฐ์ดํ„ฐ ์ œ๊ณต +- ๋ชจ๋“  ์Šคํ‚ค๋งˆ์— example ๋ฐ์ดํ„ฐ๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค +- Swagger UI์—์„œ ์ฆ‰์‹œ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค +- ์„ฑ๊ณต/์‹คํŒจ ์‹œ๋‚˜๋ฆฌ์˜ค ๋ชจ๋‘ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค + +--- + +## 3. ์„œ๋น„์Šค๋ณ„ API ๋ช…์„ธ + +### 3.1 User Service (์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๋งค์žฅ์ •๋ณด ๊ด€๋ฆฌ) + +**ํŒŒ์ผ**: `user-service-api.yaml` +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-USER-010, 020, 030, 040 + +#### API ์—”๋“œํฌ์ธํŠธ (7๊ฐœ) + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ธ์ฆ | +|--------|------|------|-----------|------| +| POST | /api/users/register | ํšŒ์›๊ฐ€์ž… | UFR-USER-010 | - | +| POST | /api/users/login | ๋กœ๊ทธ์ธ | UFR-USER-020 | - | +| POST | /api/users/logout | ๋กœ๊ทธ์•„์›ƒ | UFR-USER-040 | JWT | +| GET | /api/users/profile | ํ”„๋กœํ•„ ์กฐํšŒ | UFR-USER-030 | JWT | +| PUT | /api/users/profile | ํ”„๋กœํ•„ ์ˆ˜์ • | UFR-USER-030 | JWT | +| PUT | /api/users/password | ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ | UFR-USER-030 | JWT | +| GET | /api/users/{userId}/store | ๋งค์žฅ์ •๋ณด ์กฐํšŒ (์„œ๋น„์Šค ์—ฐ๋™์šฉ) | - | JWT | + +#### ์ฃผ์š” ๊ธฐ๋Šฅ +- JWT ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ (TTL 7์ผ) +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ (๊ตญ์„ธ์ฒญ API ์—ฐ๋™) +- Redis ์„ธ์…˜ ๊ด€๋ฆฌ +- BCrypt ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ +- AES-256-GCM ์‚ฌ์—…์ž๋ฒˆํ˜ธ ์•”ํ˜ธํ™” + +#### ์ฃผ์š” ์Šคํ‚ค๋งˆ +- `UserRegisterRequest`: ํšŒ์›๊ฐ€์ž… ์š”์ฒญ +- `UserLoginRequest`: ๋กœ๊ทธ์ธ ์š”์ฒญ +- `UserProfileResponse`: ํ”„๋กœํ•„ ์‘๋‹ต +- `StoreInfoResponse`: ๋งค์žฅ์ •๋ณด ์‘๋‹ต + +--- + +### 3.2 Event Service (์ด๋ฒคํŠธ ์ „์ฒด ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ) + +**ํŒŒ์ผ**: `event-service-api.yaml` +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-EVENT-010 ~ 070 + +#### API ์—”๋“œํฌ์ธํŠธ (14๊ฐœ) + +**Dashboard & Event List:** +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ธ์ฆ | +|--------|------|------|-----------|------| +| GET | /api/events | ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ | UFR-EVENT-010, 070 | JWT | +| GET | /api/events/{eventId} | ์ด๋ฒคํŠธ ์ƒ์„ธ ์กฐํšŒ | UFR-EVENT-060 | JWT | + +**Event Creation Flow (5 Steps):** +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ธ์ฆ | +|--------|------|------|-----------|------| +| POST | /api/events/objectives | Step 1: ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ | UFR-EVENT-020 | JWT | +| POST | /api/events/{eventId}/ai-recommendations | Step 2: AI ์ถ”์ฒœ ์š”์ฒญ | UFR-EVENT-030 | JWT | +| PUT | /api/events/{eventId}/recommendations | Step 2-2: AI ์ถ”์ฒœ ์„ ํƒ | UFR-EVENT-030 | JWT | +| POST | /api/events/{eventId}/images | Step 3: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ | UFR-CONT-010 | JWT | +| PUT | /api/events/{eventId}/images/{imageId}/select | Step 3-2: ์ด๋ฏธ์ง€ ์„ ํƒ | UFR-CONT-010 | JWT | +| PUT | /api/events/{eventId}/images/{imageId}/edit | Step 3-3: ์ด๋ฏธ์ง€ ํŽธ์ง‘ | UFR-CONT-020 | JWT | +| PUT | /api/events/{eventId}/channels | Step 4: ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ | UFR-EVENT-040 | JWT | +| POST | /api/events/{eventId}/publish | Step 5: ์ตœ์ข… ์Šน์ธ ๋ฐ ๋ฐฐํฌ | UFR-EVENT-050 | JWT | + +**Event Management:** +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ธ์ฆ | +|--------|------|------|-----------|------| +| PUT | /api/events/{eventId} | ์ด๋ฒคํŠธ ์ˆ˜์ • | UFR-EVENT-060 | JWT | +| DELETE | /api/events/{eventId} | ์ด๋ฒคํŠธ ์‚ญ์ œ | UFR-EVENT-070 | JWT | +| POST | /api/events/{eventId}/end | ์ด๋ฒคํŠธ ์กฐ๊ธฐ ์ข…๋ฃŒ | UFR-EVENT-060 | JWT | + +**Job Status:** +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ธ์ฆ | +|--------|------|------|-----------|------| +| GET | /api/jobs/{jobId} | Job ์ƒํƒœ ํด๋ง | UFR-EVENT-030, UFR-CONT-010 | JWT | + +#### ์ฃผ์š” ๊ธฐ๋Šฅ +- ์ด๋ฒคํŠธ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ (DRAFT โ†’ PUBLISHED โ†’ ENDED) +- Kafka Job ๋ฐœํ–‰ (ai-event-generation-job, image-generation-job) +- Kafka Event ๋ฐœํ–‰ (EventCreated) +- Distribution Service ๋™๊ธฐ ํ˜ธ์ถœ +- Redis ๊ธฐ๋ฐ˜ AI/์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ ์บ์‹ฑ +- Job ์ƒํƒœ ํด๋ง ๋ฉ”์ปค๋‹ˆ์ฆ˜ (PENDING, PROCESSING, COMPLETED, FAILED) + +#### ์ฃผ์š” ์Šคํ‚ค๋งˆ +- `EventObjectiveRequest`: ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ +- `EventResponse`: ์ด๋ฒคํŠธ ์‘๋‹ต +- `JobStatusResponse`: Job ์ƒํƒœ ์‘๋‹ต +- `AIRecommendationSelection`: AI ์ถ”์ฒœ ์„ ํƒ +- `ChannelSelectionRequest`: ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ + +--- + +### 3.3 AI Service (AI ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ถ”์ฒœ) + +**ํŒŒ์ผ**: `ai-service-api.yaml` +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-AI-010 + +#### API ์—”๋“œํฌ์ธํŠธ (3๊ฐœ) + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ธ์ฆ | +|--------|------|------|-----------|------| +| GET | /health | ์„œ๋น„์Šค ํ—ฌ์Šค์ฒดํฌ | - | - | +| GET | /internal/jobs/{jobId}/status | Job ์ƒํƒœ ์กฐํšŒ (๋‚ด๋ถ€ API) | UFR-AI-010 | JWT | +| GET | /internal/recommendations/{eventId} | AI ์ถ”์ฒœ ๊ฒฐ๊ณผ ์กฐํšŒ (๋‚ด๋ถ€ API) | UFR-AI-010 | JWT | + +#### Kafka Consumer (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) +- **Topic**: `ai-event-generation-job` +- **Consumer Group**: `ai-service-consumers` +- **์ฒ˜๋ฆฌ ์‹œ๊ฐ„**: ์ตœ๋Œ€ 5๋ถ„ +- **๊ฒฐ๊ณผ ์ €์žฅ**: Redis (TTL 24์‹œ๊ฐ„) + +#### ์ฃผ์š” ๊ธฐ๋Šฅ +- ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ ๋ถ„์„ +- 3๊ฐ€์ง€ ์ฐจ๋ณ„ํ™”๋œ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ ์ƒ์„ฑ +- ์˜ˆ์ƒ ์„ฑ๊ณผ ๊ณ„์‚ฐ (์ฐธ์—ฌ์ž ์ˆ˜, ROI, ๋งค์ถœ ์ฆ๊ฐ€์œจ) +- Circuit Breaker ํŒจํ„ด (5๋ถ„ timeout, ์บ์‹œ fallback) +- Claude API / GPT-4 API ์—ฐ๋™ + +#### ์ฃผ์š” ์Šคํ‚ค๋งˆ +- `KafkaAIJobMessage`: Kafka Job ์ž…๋ ฅ +- `AIRecommendationResult`: AI ์ถ”์ฒœ ๊ฒฐ๊ณผ (ํŠธ๋ Œ๋“œ ๋ถ„์„ + 3๊ฐ€์ง€ ์˜ต์…˜) +- `TrendAnalysis`: ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ +- `EventRecommendation`: ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ (์ปจ์…‰, ๊ฒฝํ’ˆ, ์ฐธ์—ฌ๋ฐฉ๋ฒ•, ์˜ˆ์ƒ์„ฑ๊ณผ) + +--- + +### 3.4 Content Service (SNS ์ฝ˜ํ…์ธ  ์ƒ์„ฑ) + +**ํŒŒ์ผ**: `content-service-api.yaml` +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-CONT-010, 020 + +#### API ์—”๋“œํฌ์ธํŠธ (6๊ฐœ) + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ธ์ฆ | +|--------|------|------|-----------|------| +| POST | /api/content/images/generate | ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ (๋น„๋™๊ธฐ) | UFR-CONT-010 | JWT | +| GET | /api/content/images/jobs/{jobId} | Job ์ƒํƒœ ํด๋ง | UFR-CONT-010 | JWT | +| GET | /api/content/events/{eventDraftId} | ์ด๋ฒคํŠธ ์ „์ฒด ์ฝ˜ํ…์ธ  ์กฐํšŒ | UFR-CONT-020 | JWT | +| GET | /api/content/events/{eventDraftId}/images | ์ด๋ฏธ์ง€ ๋ชฉ๋ก ์กฐํšŒ | UFR-CONT-020 | JWT | +| GET | /api/content/images/{imageId} | ์ด๋ฏธ์ง€ ์ƒ์„ธ ์กฐํšŒ | UFR-CONT-020 | JWT | +| POST | /api/content/images/{imageId}/regenerate | ์ด๋ฏธ์ง€ ์žฌ์ƒ์„ฑ | UFR-CONT-020 | JWT | + +#### Kafka Consumer (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) +- **Topic**: `image-generation-job` +- **Consumer Group**: `content-service-consumers` +- **์ฒ˜๋ฆฌ ์‹œ๊ฐ„**: ์ตœ๋Œ€ 5๋ถ„ +- **๊ฒฐ๊ณผ ์ €์žฅ**: Redis (CDN URL, TTL 7์ผ) + +#### ์ฃผ์š” ๊ธฐ๋Šฅ +- 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ (SIMPLE, FANCY, TRENDY) +- ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™” (Instagram 1080x1080, Naver 800x600, Kakao 800x800) +- Circuit Breaker ํŒจํ„ด (Stable Diffusion โ†’ DALL-E โ†’ Default Template) +- Azure Blob Storage (CDN) ์—ฐ๋™ +- Redis ๊ธฐ๋ฐ˜ AI ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ + +#### ์ฃผ์š” ์Šคํ‚ค๋งˆ +- `ImageGenerationJob`: Kafka Job ์ž…๋ ฅ +- `ImageGenerationRequest`: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ +- `GeneratedImage`: ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ (style, platform, CDN URL) +- `ContentResponse`: ์ „์ฒด ์ฝ˜ํ…์ธ  ์‘๋‹ต + +--- + +### 3.5 Distribution Service (๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ ๊ด€๋ฆฌ) + +**ํŒŒ์ผ**: `distribution-service-api.yaml` +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-DIST-010, 020 + +#### API ์—”๋“œํฌ์ธํŠธ (2๊ฐœ) + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ธ์ฆ | +|--------|------|------|-----------|------| +| POST | /api/distribution/distribute | ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ (๋™๊ธฐ) | UFR-DIST-010 | JWT | +| GET | /api/distribution/{eventId}/status | ๋ฐฐํฌ ์ƒํƒœ ์กฐํšŒ | UFR-DIST-020 | JWT | + +#### ์ฃผ์š” ๊ธฐ๋Šฅ +- **๋ฐฐํฌ ์ฑ„๋„**: ์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, Instagram, Naver Blog, Kakao Channel +- **๋ณ‘๋ ฌ ๋ฐฐํฌ**: 6๊ฐœ ์ฑ„๋„ ๋™์‹œ ๋ฐฐํฌ (1๋ถ„ ์ด๋‚ด) +- **Resilience ํŒจํ„ด**: + - Circuit Breaker: ์ฑ„๋„๋ณ„ ๋…๋ฆฝ ์ ์šฉ + - Retry: ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„ (์ง€์ˆ˜ ๋ฐฑ์˜คํ”„: 1s, 2s, 4s) + - Bulkhead: ์ฑ„๋„๋ณ„ ์Šค๋ ˆ๋“œ ํ’€ ๊ฒฉ๋ฆฌ + - Fallback: ์‹คํŒจ ์ฑ„๋„ ์Šคํ‚ต + ์•Œ๋ฆผ +- **Kafka Event ๋ฐœํ–‰**: DistributionCompleted +- **๋กœ๊น…**: Event DB์— distribution_logs ์ €์žฅ + +#### ์ฃผ์š” ์Šคํ‚ค๋งˆ +- `DistributionRequest`: ๋ฐฐํฌ ์š”์ฒญ +- `DistributionResponse`: ๋ฐฐํฌ ์‘๋‹ต (์ฑ„๋„๋ณ„ ๊ฒฐ๊ณผ) +- `DistributionStatusResponse`: ๋ฐฐํฌ ์ƒํƒœ +- `ChannelDistributionResult`: ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ๊ฒฐ๊ณผ + +--- + +### 3.6 Participation Service (์ด๋ฒคํŠธ ์ฐธ์—ฌ ๋ฐ ๋‹น์ฒจ์ž ๊ด€๋ฆฌ) + +**ํŒŒ์ผ**: `participation-service-api.yaml` +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-PART-010, 020, 030 + +#### API ์—”๋“œํฌ์ธํŠธ (5๊ฐœ) + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ธ์ฆ | +|--------|------|------|-----------|------| +| POST | /api/events/{eventId}/participate | ์ด๋ฒคํŠธ ์ฐธ์—ฌ | UFR-PART-010 | - | +| GET | /api/events/{eventId}/participants | ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ | UFR-PART-020 | JWT | +| GET | /api/events/{eventId}/participants/{participantId} | ์ฐธ์—ฌ์ž ์ƒ์„ธ ์กฐํšŒ | UFR-PART-020 | JWT | +| POST | /api/events/{eventId}/draw-winners | ๋‹น์ฒจ์ž ์ถ”์ฒจ | UFR-PART-030 | JWT | +| GET | /api/events/{eventId}/winners | ๋‹น์ฒจ์ž ๋ชฉ๋ก ์กฐํšŒ | UFR-PART-030 | JWT | + +#### ์ฃผ์š” ๊ธฐ๋Šฅ +- ์ค‘๋ณต ์ฐธ์—ฌ ์ฒดํฌ (์ „ํ™”๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜) +- ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์  ์ ์šฉ +- ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฌด์ž‘์œ„ ์ถ”์ฒจ +- Kafka Event ๋ฐœํ–‰ (ParticipantRegistered) +- ๊ฐœ์ธ์ •๋ณด ์ˆ˜์ง‘/์ด์šฉ ๋™์˜ ๊ด€๋ฆฌ +- ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ง€์› + +#### ์ฃผ์š” ์Šคํ‚ค๋งˆ +- `ParticipationRequest`: ์ฐธ์—ฌ ์š”์ฒญ +- `ParticipationResponse`: ์ฐธ์—ฌ ์‘๋‹ต (์‘๋ชจ๋ฒˆํ˜ธ) +- `ParticipantListResponse`: ์ฐธ์—ฌ์ž ๋ชฉ๋ก +- `WinnerDrawRequest`: ๋‹น์ฒจ์ž ์ถ”์ฒจ ์š”์ฒญ +- `WinnerResponse`: ๋‹น์ฒจ์ž ์ •๋ณด + +--- + +### 3.7 Analytics Service (์‹ค์‹œ๊ฐ„ ํšจ๊ณผ ์ธก์ • ๋ฐ ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ) + +**ํŒŒ์ผ**: `analytics-service-api.yaml` +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-ANAL-010 + +#### API ์—”๋“œํฌ์ธํŠธ (4๊ฐœ) + +| ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ธ์ฆ | +|--------|------|------|-----------|------| +| GET | /api/events/{eventId}/analytics | ์„ฑ๊ณผ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ | UFR-ANAL-010 | JWT | +| GET | /api/events/{eventId}/analytics/channels | ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ | UFR-ANAL-010 | JWT | +| GET | /api/events/{eventId}/analytics/timeline | ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด | UFR-ANAL-010 | JWT | +| GET | /api/events/{eventId}/analytics/roi | ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  ์ƒ์„ธ | UFR-ANAL-010 | JWT | + +#### Kafka Event ๊ตฌ๋… +- **EventCreated**: ์ด๋ฒคํŠธ ๊ธฐ๋ณธ ํ†ต๊ณ„ ์ดˆ๊ธฐํ™” +- **ParticipantRegistered**: ์‹ค์‹œ๊ฐ„ ์ฐธ์—ฌ์ž ์ˆ˜ ์ฆ๊ฐ€ +- **DistributionCompleted**: ๋ฐฐํฌ ์ฑ„๋„ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ + +#### ์ฃผ์š” ๊ธฐ๋Šฅ +- **์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ**: Redis ์บ์‹ฑ (TTL 5๋ถ„) +- **์™ธ๋ถ€ API ํ†ตํ•ฉ**: ์šฐ๋ฆฌ๋™๋„คTV, ์ง€๋‹ˆTV, SNS APIs (์กฐํšŒ์ˆ˜, ๋…ธ์ถœ์ˆ˜, ์†Œ์…œ ์ธํ„ฐ๋ž™์…˜) +- **Circuit Breaker**: ์™ธ๋ถ€ API ์‹คํŒจ ์‹œ ์บ์‹œ fallback +- **ROI ๊ณ„์‚ฐ**: ๋น„์šฉ ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  ์ž๋™ ๊ณ„์‚ฐ +- **์„ฑ๊ณผ ์ง‘๊ณ„**: ์ฑ„๋„๋ณ„, ์‹œ๊ฐ„๋Œ€๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ + +#### ์ฃผ์š” ์Šคํ‚ค๋งˆ +- `AnalyticsDashboardResponse`: ๋Œ€์‹œ๋ณด๋“œ ์ „์ฒด ๋ฐ์ดํ„ฐ +- `ChannelPerformanceResponse`: ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ +- `TimelineDataResponse`: ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด +- `RoiDetailResponse`: ROI ์ƒ์„ธ ๋ถ„์„ + +--- + +## 4. API ํ†ตํ•ฉ ๊ฐ€์ด๋“œ + +### 4.1 ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ (Event-Driven) + +``` +1. ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ (Event Service) + POST /api/events/objectives + โ†’ EventCreated ์ด๋ฒคํŠธ ๋ฐœํ–‰ (Kafka) + โ†’ Analytics Service ๊ตฌ๋… (ํ†ต๊ณ„ ์ดˆ๊ธฐํ™”) + +2. AI ์ถ”์ฒœ ์š”์ฒญ (Event Service โ†’ AI Service) + POST /api/events/{eventId}/ai-recommendations + โ†’ ai-event-generation-job ๋ฐœํ–‰ (Kafka) + โ†’ AI Service ๊ตฌ๋… ๋ฐ ์ฒ˜๋ฆฌ (๋น„๋™๊ธฐ) + โ†’ Redis์— ๊ฒฐ๊ณผ ์ €์žฅ (TTL 24์‹œ๊ฐ„) + โ†’ ํด๋ผ์ด์–ธํŠธ ํด๋ง: GET /api/jobs/{jobId} + +3. AI ์ถ”์ฒœ ์„ ํƒ (Event Service) + PUT /api/events/{eventId}/recommendations + โ†’ Redis์—์„œ AI ์ถ”์ฒœ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ + โ†’ Event DB์— ์„ ํƒ๋œ ์ถ”์ฒœ ์ €์žฅ + +4. ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ (Event Service โ†’ Content Service) + POST /api/events/{eventId}/images + โ†’ image-generation-job ๋ฐœํ–‰ (Kafka) + โ†’ Content Service ๊ตฌ๋… ๋ฐ ์ฒ˜๋ฆฌ (๋น„๋™๊ธฐ) + โ†’ Redis์—์„œ AI ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ + โ†’ CDN์— ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ + โ†’ Redis์— CDN URL ์ €์žฅ (TTL 7์ผ) + โ†’ ํด๋ผ์ด์–ธํŠธ ํด๋ง: GET /api/jobs/{jobId} + +5. ์ด๋ฏธ์ง€ ์„ ํƒ ๋ฐ ํŽธ์ง‘ (Event Service) + PUT /api/events/{eventId}/images/{imageId}/select + PUT /api/events/{eventId}/images/{imageId}/edit + +6. ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ (Event Service) + PUT /api/events/{eventId}/channels + +7. ์ตœ์ข… ์Šน์ธ ๋ฐ ๋ฐฐํฌ (Event Service โ†’ Distribution Service) + POST /api/events/{eventId}/publish + โ†’ Distribution Service ๋™๊ธฐ ํ˜ธ์ถœ: POST /api/distribution/distribute + โ†’ ๋‹ค์ค‘ ์ฑ„๋„ ๋ณ‘๋ ฌ ๋ฐฐํฌ (1๋ถ„ ์ด๋‚ด) + โ†’ DistributionCompleted ์ด๋ฒคํŠธ ๋ฐœํ–‰ (Kafka) + โ†’ Analytics Service ๊ตฌ๋… (๋ฐฐํฌ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ) +``` + +### 4.2 ๊ณ ๊ฐ ์ฐธ์—ฌ ํ”Œ๋กœ์šฐ (Event-Driven) + +``` +1. ์ด๋ฒคํŠธ ์ฐธ์—ฌ (Participation Service) + POST /api/events/{eventId}/participate + โ†’ ์ค‘๋ณต ์ฐธ์—ฌ ์ฒดํฌ + โ†’ Participation DB ์ €์žฅ + โ†’ ParticipantRegistered ์ด๋ฒคํŠธ ๋ฐœํ–‰ (Kafka) + โ†’ Analytics Service ๊ตฌ๋… (์ฐธ์—ฌ์ž ์ˆ˜ ์‹ค์‹œ๊ฐ„ ์ฆ๊ฐ€) + +2. ๋‹น์ฒจ์ž ์ถ”์ฒจ (Participation Service) + POST /api/events/{eventId}/draw-winners + โ†’ ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฌด์ž‘์œ„ ์ถ”์ฒจ + โ†’ Winners DB ์ €์žฅ +``` + +### 4.3 ์„ฑ๊ณผ ๋ถ„์„ ํ”Œ๋กœ์šฐ (Event-Driven) + +``` +1. ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ (Analytics Service) + GET /api/events/{eventId}/analytics + โ†’ Redis ์บ์‹œ ํ™•์ธ (TTL 5๋ถ„) + โ†’ ์บ์‹œ HIT: ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + โ†’ ์บ์‹œ MISS: + - Analytics DB ์กฐํšŒ (์ด๋ฒคํŠธ/์ฐธ์—ฌ ํ†ต๊ณ„) + - ์™ธ๋ถ€ APIs ์กฐํšŒ (์šฐ๋ฆฌ๋™๋„คTV, ์ง€๋‹ˆTV, SNS) [Circuit Breaker] + - Redis ์บ์‹ฑ ํ›„ ๋ฐ˜ํ™˜ + +2. Kafka ์ด๋ฒคํŠธ ๊ตฌ๋… (Analytics Service Background) + - EventCreated ๊ตฌ๋… โ†’ ์ด๋ฒคํŠธ ๊ธฐ๋ณธ ์ •๋ณด ์ดˆ๊ธฐํ™” + - ParticipantRegistered ๊ตฌ๋… โ†’ ์ฐธ์—ฌ์ž ์ˆ˜ ์‹ค์‹œ๊ฐ„ ์ฆ๊ฐ€ + - DistributionCompleted ๊ตฌ๋… โ†’ ๋ฐฐํฌ ์ฑ„๋„ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ + - ์บ์‹œ ๋ฌดํšจํ™” โ†’ ๋‹ค์Œ ์กฐํšŒ ์‹œ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  +``` + +### 4.4 ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  ํŒจํ„ด + +| ํŒจํ„ด | ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค | ํ†ต์‹  ๋ฐฉ์‹ | ์˜ˆ์‹œ | +|------|-------------|----------|------| +| **๋™๊ธฐ REST API** | ์ฆ‰์‹œ ์‘๋‹ต ํ•„์š” | HTTP/JSON | Distribution Service ๋ฐฐํฌ ์š”์ฒญ | +| **Kafka Job Topics** | ์žฅ์‹œ๊ฐ„ ๋น„๋™๊ธฐ ์ž‘์—… | Kafka ๋ฉ”์‹œ์ง€ ํ | AI ์ถ”์ฒœ, ์ด๋ฏธ์ง€ ์ƒ์„ฑ | +| **Kafka Event Topics** | ์ƒํƒœ ๋ณ€๊ฒฝ ์•Œ๋ฆผ | Kafka Pub/Sub | EventCreated, ParticipantRegistered | +| **Redis Cache** | ๋ฐ์ดํ„ฐ ๊ณต์œ  | Redis Get/Set | AI ๊ฒฐ๊ณผ, ์ด๋ฏธ์ง€ URL | + +--- + +## 5. ๋ณด์•ˆ ๋ฐ ์ธ์ฆ + +### 5.1 JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ + +**ํ† ํฐ ๋ฐœ๊ธ‰:** +- User Service์—์„œ ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… ์‹œ JWT ํ† ํฐ ๋ฐœ๊ธ‰ +- ํ† ํฐ ๋งŒ๋ฃŒ ์‹œ๊ฐ„: 7์ผ +- Redis์— ์„ธ์…˜ ์ •๋ณด ์ €์žฅ (TTL 7์ผ) + +**ํ† ํฐ ๊ฒ€์ฆ:** +- API Gateway์—์„œ ๋ชจ๋“  ์š”์ฒญ์˜ JWT ํ† ํฐ ๊ฒ€์ฆ +- Authorization ํ—ค๋”: `Bearer {token}` +- ๊ฒ€์ฆ ์‹คํŒจ ์‹œ 401 Unauthorized ์‘๋‹ต + +**๋ณดํ˜ธ๋œ ์—”๋“œํฌ์ธํŠธ:** +- ๋ชจ๋“  API (ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ, ์ด๋ฒคํŠธ ์ฐธ์—ฌ ์ œ์™ธ) + +### 5.2 ๋ฏผ๊ฐ ์ •๋ณด ์•”ํ˜ธํ™” + +- **๋น„๋ฐ€๋ฒˆํ˜ธ**: BCrypt ํ•ด์‹ฑ (Cost Factor: 10) +- **์‚ฌ์—…์ž๋ฒˆํ˜ธ**: AES-256-GCM ์•”ํ˜ธํ™” +- **๊ฐœ์ธ์ •๋ณด**: ์ „ํ™”๋ฒˆํ˜ธ ๋งˆ์Šคํ‚น (010-****-1234) + +### 5.3 API Rate Limiting + +- API Gateway์—์„œ ์‚ฌ์šฉ์ž๋‹น 100 req/min ์ œํ•œ +- Redis ๊ธฐ๋ฐ˜ Rate Limiting ๊ตฌํ˜„ + +--- + +## 6. ์—๋Ÿฌ ์ฒ˜๋ฆฌ + +### 6.1 ํ‘œ์ค€ ์—๋Ÿฌ ์‘๋‹ต ํฌ๋งท + +```json +{ + "success": false, + "errorCode": "ERROR_CODE", + "message": "์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€", + "details": "์ƒ์„ธ ์—๋Ÿฌ ์ •๋ณด (์„ ํƒ)", + "timestamp": "2025-10-23T16:30:00Z" +} +``` + +### 6.2 HTTP ์ƒํƒœ ์ฝ”๋“œ + +| ์ƒํƒœ ์ฝ”๋“œ | ์„ค๋ช… | ์‚ฌ์šฉ ์˜ˆ์‹œ | +|----------|------|----------| +| 200 OK | ์„ฑ๊ณต | GET ์š”์ฒญ ์„ฑ๊ณต | +| 201 Created | ์ƒ์„ฑ ์„ฑ๊ณต | POST ์š”์ฒญ์œผ๋กœ ๋ฆฌ์†Œ์Šค ์ƒ์„ฑ | +| 400 Bad Request | ์ž˜๋ชป๋œ ์š”์ฒญ | ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ | +| 401 Unauthorized | ์ธ์ฆ ์‹คํŒจ | JWT ํ† ํฐ ์—†์Œ/๋งŒ๋ฃŒ | +| 403 Forbidden | ๊ถŒํ•œ ์—†์Œ | ์ ‘๊ทผ ๊ถŒํ•œ ๋ถ€์กฑ | +| 404 Not Found | ๋ฆฌ์†Œ์Šค ์—†์Œ | ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ด๋ฒคํŠธ ์กฐํšŒ | +| 409 Conflict | ์ถฉ๋Œ | ์ค‘๋ณต ์ฐธ์—ฌ, ๋™์‹œ์„ฑ ๋ฌธ์ œ | +| 500 Internal Server Error | ์„œ๋ฒ„ ์˜ค๋ฅ˜ | ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ | +| 503 Service Unavailable | ์„œ๋น„์Šค ๋ถˆ๊ฐ€ | Circuit Breaker Open | + +### 6.3 ์„œ๋น„์Šค๋ณ„ ์ฃผ์š” ์—๋Ÿฌ ์ฝ”๋“œ + +**User Service:** +- `USER_001`: ์ค‘๋ณต ์‚ฌ์šฉ์ž +- `USER_002`: ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ์‹คํŒจ +- `USER_003`: ์‚ฌ์šฉ์ž ์—†์Œ +- `AUTH_001`: ์ธ์ฆ ์‹คํŒจ +- `AUTH_002`: ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ + +**Event Service:** +- `EVENT_001`: ์ด๋ฒคํŠธ ์—†์Œ +- `EVENT_002`: ์œ ํšจํ•˜์ง€ ์•Š์€ ์ƒํƒœ ์ „ํ™˜ +- `EVENT_003`: ํ•„์ˆ˜ ๋ฐ์ดํ„ฐ ๋ˆ„๋ฝ (AI ์ถ”์ฒœ, ์ด๋ฏธ์ง€) +- `JOB_001`: Job ์—†์Œ +- `JOB_002`: Job ์‹คํŒจ + +**Participation Service:** +- `PART_001`: ์ค‘๋ณต ์ฐธ์—ฌ +- `PART_002`: ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„ ์•„๋‹˜ +- `PART_003`: ์ฐธ์—ฌ์ž ์—†์Œ + +**Distribution Service:** +- `DIST_001`: ๋ฐฐํฌ ์‹คํŒจ +- `DIST_002`: Circuit Breaker Open + +**Analytics Service:** +- `ANALYTICS_001`: ๋ฐ์ดํ„ฐ ์—†์Œ +- `EXTERNAL_API_ERROR`: ์™ธ๋ถ€ API ์žฅ์•  + +--- + +## 7. API ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ + +### 7.1 Swagger UI๋ฅผ ํ†ตํ•œ ํ…Œ์ŠคํŠธ + +**๋ฐฉ๋ฒ• 1: Swagger Editor** +1. https://editor.swagger.io/ ์ ‘์† +2. ๊ฐ ์„œ๋น„์Šค์˜ YAML ํŒŒ์ผ ๋‚ด์šฉ ๋ถ™์—ฌ๋„ฃ๊ธฐ +3. ์šฐ์ธก Swagger UI์—์„œ API ํ…Œ์ŠคํŠธ + +**๋ฐฉ๋ฒ• 2: SwaggerHub** +1. ๊ฐ API ๋ช…์„ธ์˜ `servers` ์„น์…˜์— SwaggerHub Mock Server URL ํฌํ•จ +2. Mock Server๋ฅผ ํ†ตํ•œ ์ฆ‰์‹œ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ + +**๋ฐฉ๋ฒ• 3: Redocly** +```bash +# ๊ฐ API ๋ช…์„ธ ๊ฒ€์ฆ +npx @redocly/cli lint design/backend/api/*.yaml + +# ๋ฌธ์„œ HTML ์ƒ์„ฑ +npx @redocly/cli build-docs design/backend/api/user-service-api.yaml \ + --output docs/user-service-api.html +``` + +### 7.2 ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค ์˜ˆ์‹œ + +**1. ํšŒ์›๊ฐ€์ž… โ†’ ๋กœ๊ทธ์ธ โ†’ ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ** +```bash +# 1. ํšŒ์›๊ฐ€์ž… +POST /api/users/register +{ + "name": "๊น€์‚ฌ์žฅ", + "phoneNumber": "010-1234-5678", + "email": "owner@example.com", + "password": "SecurePass123!", + "store": { + "name": "๋ง›์žˆ๋Š” ๊ณ ๊นƒ์ง‘", + "industry": "RESTAURANT", + "address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123", + "businessNumber": "123-45-67890" + } +} + +# 2. ๋กœ๊ทธ์ธ +POST /api/users/login +{ + "phoneNumber": "010-1234-5678", + "password": "SecurePass123!" +} +# โ†’ JWT ํ† ํฐ ์ˆ˜์‹  + +# 3. ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ +POST /api/events/objectives +Authorization: Bearer {token} +{ + "objective": "NEW_CUSTOMER_ACQUISITION" +} +# โ†’ eventId ์ˆ˜์‹  + +# 4. AI ์ถ”์ฒœ ์š”์ฒญ +POST /api/events/{eventId}/ai-recommendations +Authorization: Bearer {token} +# โ†’ jobId ์ˆ˜์‹  + +# 5. Job ์ƒํƒœ ํด๋ง (5์ดˆ ๊ฐ„๊ฒฉ) +GET /api/jobs/{jobId} +Authorization: Bearer {token} +# โ†’ status: COMPLETED ํ™•์ธ + +# 6. AI ์ถ”์ฒœ ์„ ํƒ +PUT /api/events/{eventId}/recommendations +Authorization: Bearer {token} +{ + "selectedOption": 1, + "customization": { + "title": "๋ด„๋งž์ด ์‚ผ๊ฒน์‚ด 50% ํ• ์ธ ์ด๋ฒคํŠธ", + "prizeName": "์‚ผ๊ฒน์‚ด 1์ธ๋ถ„ ๋ฌด๋ฃŒ" + } +} + +# ... (์ด๋ฏธ์ง€ ์ƒ์„ฑ, ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ, ์ตœ์ข… ์Šน์ธ) +``` + +### 7.3 Mock ๋ฐ์ดํ„ฐ ํ™œ์šฉ + +- ๋ชจ๋“  API ๋ช…์„ธ์— example ๋ฐ์ดํ„ฐ ํฌํ•จ +- Swagger UI์˜ "Try it out" ๊ธฐ๋Šฅ์œผ๋กœ ์ฆ‰์‹œ ํ…Œ์ŠคํŠธ +- ์„ฑ๊ณต/์‹คํŒจ ์‹œ๋‚˜๋ฆฌ์˜ค ๋ชจ๋‘ example ์ œ๊ณต + +### 7.4 ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๋„๊ตฌ + +**Postman Collection ์ƒ์„ฑ:** +```bash +# OpenAPI ๋ช…์„ธ๋ฅผ Postman Collection์œผ๋กœ ๋ณ€ํ™˜ +npx openapi-to-postmanv2 -s design/backend/api/user-service-api.yaml \ + -o postman/user-service-collection.json +``` + +**Newman (CLI ํ…Œ์ŠคํŠธ ์‹คํ–‰):** +```bash +# Postman Collection ์‹คํ–‰ +newman run postman/user-service-collection.json \ + --environment postman/dev-environment.json +``` + +--- + +## ๋ถ€๋ก + +### A. ํŒŒ์ผ ํ†ต๊ณ„ + +| ์„œ๋น„์Šค | ํŒŒ์ผ๋ช… | ํฌ๊ธฐ | ๋ผ์ธ ์ˆ˜ | API ์ˆ˜ | +|--------|--------|------|--------|--------| +| User | user-service-api.yaml | 31KB | 1,011 | 7 | +| Event | event-service-api.yaml | 41KB | 1,373 | 14 | +| AI | ai-service-api.yaml | 26KB | 847 | 3 | +| Content | content-service-api.yaml | 37KB | 1,158 | 6 | +| Distribution | distribution-service-api.yaml | 21KB | 653 | 2 | +| Participation | participation-service-api.yaml | 25KB | 820 | 5 | +| Analytics | analytics-service-api.yaml | 28KB | 1,050 | 4 | +| **ํ•ฉ๊ณ„** | - | **209KB** | **6,912** | **41** | + +### B. ์ฃผ์š” ์˜์‚ฌ๊ฒฐ์ • + +1. **OpenAPI 3.0 ํ‘œ์ค€ ์ฑ„ํƒ**: ์—…๊ณ„ ํ‘œ์ค€ ์ค€์ˆ˜, ์ž๋™ ์ฝ”๋“œ ์ƒ์„ฑ ์ง€์› +2. **์„œ๋น„์Šค๋ณ„ ๋…๋ฆฝ ๋ช…์„ธ**: ์„œ๋น„์Šค ๋…๋ฆฝ์„ฑ ๋ณด์žฅ, ๋…๋ฆฝ ๋ฐฐํฌ ๊ฐ€๋Šฅ +3. **์œ ์ €์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ์„ค๊ณ„**: x-user-story ํ•„๋“œ๋กœ ์ถ”์ ์„ฑ ํ™•๋ณด +4. **Example ๋ฐ์ดํ„ฐ ํฌํ•จ**: Swagger UI ์ฆ‰์‹œ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ +5. **JWT ์ธ์ฆ ํ‘œ์ค€ํ™”**: ๋ชจ๋“  ์„œ๋น„์Šค์—์„œ ์ผ๊ด€๋œ ์ธ์ฆ ๋ฐฉ์‹ +6. **์—๋Ÿฌ ์‘๋‹ต ํ‘œ์ค€ํ™”**: ์ผ๊ด€๋œ ์—๋Ÿฌ ์‘๋‹ต ํฌ๋งท +7. **Kafka + Redis ํ†ตํ•ฉ**: Event-Driven ์•„ํ‚คํ…์ฒ˜ ์ง€์› +8. **Circuit Breaker ํŒจํ„ด**: ์™ธ๋ถ€ API ์žฅ์•  ๋Œ€์‘ + +### C. ๋‹ค์Œ ๋‹จ๊ณ„ + +1. **์™ธ๋ถ€ ์‹œํ€€์Šค ์„ค๊ณ„**: ์„œ๋น„์Šค ๊ฐ„ API ํ˜ธ์ถœ ํ๋ฆ„ ์ƒ์„ธ ์„ค๊ณ„ +2. **๋‚ด๋ถ€ ์‹œํ€€์Šค ์„ค๊ณ„**: ์„œ๋น„์Šค ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ํ˜ธ์ถœ ํ๋ฆ„ ์„ค๊ณ„ +3. **ํด๋ž˜์Šค ์„ค๊ณ„**: ์„œ๋น„์Šค๋ณ„ ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ ์ž‘์„ฑ +4. **๋ฐ์ดํ„ฐ ์„ค๊ณ„**: ์„œ๋น„์Šค๋ณ„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„ +5. **๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ**: OpenAPI ๋ช…์„ธ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ์ƒ์„ฑ ๋ฐ ๊ตฌํ˜„ + +--- + +**๋ฌธ์„œ ๋ฒ„์ „**: 1.0 +**์ตœ์ข… ์ˆ˜์ •์ผ**: 2025-10-23 +**์ž‘์„ฑ์ž**: System Architect diff --git a/design/backend/api/API_CONVENTION.md b/design/backend/api/API_CONVENTION.md new file mode 100644 index 0000000..6c80671 --- /dev/null +++ b/design/backend/api/API_CONVENTION.md @@ -0,0 +1,914 @@ +# OpenAPI 3.0.3 ๊ณตํ†ต ์ปจ๋ฒค์…˜ + +KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค์˜ ๋ชจ๋“  ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค API ๋ช…์„ธ์„œ์— ์ ์šฉ๋˜๋Š” ๊ณตํ†ต ์ปจ๋ฒค์…˜์ž…๋‹ˆ๋‹ค. + +## ๋ชฉ์ฐจ +1. [๊ธฐ๋ณธ ์ •๋ณด ์„น์…˜](#1-๊ธฐ๋ณธ-์ •๋ณด-์„น์…˜) +2. [์„œ๋ฒ„ ์ •์˜](#2-์„œ๋ฒ„-์ •์˜) +3. [๋ณด์•ˆ ์Šคํ‚ค๋งˆ](#3-๋ณด์•ˆ-์Šคํ‚ค๋งˆ) +4. [ํƒœ๊ทธ ๊ตฌ์„ฑ](#4-ํƒœ๊ทธ-๊ตฌ์„ฑ) +5. [์—”๋“œํฌ์ธํŠธ ์ •์˜](#5-์—”๋“œํฌ์ธํŠธ-์ •์˜) +6. [์‘๋‹ต ๊ตฌ์กฐ](#6-์‘๋‹ต-๊ตฌ์กฐ) +7. [์—๋Ÿฌ ์‘๋‹ต ๊ตฌ์กฐ](#7-์—๋Ÿฌ-์‘๋‹ต-๊ตฌ์กฐ) +8. [์Šคํ‚ค๋งˆ ์ •์˜](#8-์Šคํ‚ค๋งˆ-์ •์˜) +9. [๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ฃผ์„](#9-๋ฉ”ํƒ€๋ฐ์ดํ„ฐ-์ฃผ์„) +10. [๊ธฐ์ˆ  ๋ช…์„ธ ์„น์…˜](#10-๊ธฐ์ˆ -๋ช…์„ธ-์„น์…˜) +11. [์˜ˆ์ œ ์ž‘์„ฑ](#11-์˜ˆ์ œ-์ž‘์„ฑ) + +--- + +## 1. ๊ธฐ๋ณธ ์ •๋ณด ์„น์…˜ + +### 1.1 OpenAPI ๋ฒ„์ „ +```yaml +openapi: 3.0.3 +``` +- **ํ•„์ˆ˜**: ๋ชจ๋“  ๋ช…์„ธ์„œ๋Š” OpenAPI 3.0.3 ๋ฒ„์ „์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + +### 1.2 Info ๊ฐ์ฒด +```yaml +info: + title: {Service Name} API + description: | + KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - {Service Name} API + + {์„œ๋น„์Šค ์„ค๋ช… 1-2์ค„} + + **์ฃผ์š” ๊ธฐ๋Šฅ:** + - {๊ธฐ๋Šฅ 1} + - {๊ธฐ๋Šฅ 2} + - {๊ธฐ๋Šฅ 3} + + **๋ณด์•ˆ:** (๋ณด์•ˆ ๊ด€๋ จ ์„œ๋น„์Šค์ธ ๊ฒฝ์šฐ) + - {๋ณด์•ˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜ 1} + - {๋ณด์•ˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜ 2} + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com +``` + +**ํ•„์ˆ˜ ํ•ญ๋ชฉ:** +- `title`: "{์„œ๋น„์Šค๋ช…} API" ํ˜•์‹ +- `description`: ๋งˆํฌ๋‹ค์šด ํ˜•์‹์œผ๋กœ ์„œ๋น„์Šค ์„ค๋ช… ์ž‘์„ฑ + - ์ฒซ ์ค„: ํ”„๋กœ์ ํŠธ๋ช…๊ณผ ์„œ๋น„์Šค ์—ญํ•  + - ์„œ๋น„์Šค ์„ค๋ช… + - ์ฃผ์š” ๊ธฐ๋Šฅ ๋ชฉ๋ก (bullet points) + - ๋ณด์•ˆ ๊ด€๋ จ ์„œ๋น„์Šค์˜ ๊ฒฝ์šฐ ๋ณด์•ˆ ์„น์…˜ ์ถ”๊ฐ€ +- `version`: "1.0.0" +- `contact`: name๊ณผ email ํ•„์ˆ˜ + +--- + +## 2. ์„œ๋ฒ„ ์ •์˜ + +### 2.1 ์„œ๋ฒ„ URL ๊ตฌ์กฐ +```yaml +servers: + - url: http://localhost:{port} + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/{service}/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/{service}/v1 + description: Production Server +``` + +**ํฌํŠธ ๋ฒˆํ˜ธ ํ• ๋‹น:** +- User Service: 8081 +- Event Service: 8080 +- Content Service: 8082 +- AI Service: 8083 +- Participation Service: 8084 +- Distribution Service: 8085 +- Analytics Service: 8086 + +**URL ํŒจํ„ด:** +- Local: `http://localhost:{port}` +- Dev: `https://dev-api.kt-event-marketing.com/{service}/v1` +- Prod: `https://api.kt-event-marketing.com/{service}/v1` + +--- + +## 3. ๋ณด์•ˆ ์Šคํ‚ค๋งˆ + +### 3.1 JWT Bearer ์ธ์ฆ +```yaml +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT Bearer ํ† ํฐ ์ธ์ฆ + + **ํ˜•์‹:** Authorization: Bearer {JWT_TOKEN} + + **ํ† ํฐ ๋งŒ๋ฃŒ:** 7์ผ + + **Claims:** + - userId: ์‚ฌ์šฉ์ž ID + - role: ์‚ฌ์šฉ์ž ์—ญํ•  (OWNER) + - iat: ๋ฐœ๊ธ‰ ์‹œ๊ฐ + - exp: ๋งŒ๋ฃŒ ์‹œ๊ฐ +``` + +### 3.2 ์ „์—ญ ๋ณด์•ˆ ์ ์šฉ +```yaml +security: + - BearerAuth: [] +``` + +**์ ์šฉ ๋ฐฉ๋ฒ•:** +- ์ธ์ฆ์ด ํ•„์š”ํ•œ ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ์— `security` ์„น์…˜ ์ถ”๊ฐ€ +- ๊ณต๊ฐœ API (์˜ˆ: ๋กœ๊ทธ์ธ, ํšŒ์›๊ฐ€์ž…)๋Š” ์—”๋“œํฌ์ธํŠธ ๋ ˆ๋ฒจ์—์„œ `security: []`๋กœ ์˜ค๋ฒ„๋ผ์ด๋“œ + +--- + +## 4. ํƒœ๊ทธ ๊ตฌ์„ฑ + +### 4.1 ํƒœ๊ทธ ์ •์˜ ํŒจํ„ด +```yaml +tags: + - name: {Category Name} + description: {์นดํ…Œ๊ณ ๋ฆฌ ์„ค๋ช… (ํ•œ๊ธ€)} +``` + +**ํƒœ๊ทธ ๋ช…๋ช… ๊ทœ์น™:** +- **์˜๋ฌธ ์‚ฌ์šฉ**: ๋ช…ํ™•ํ•œ ์˜๋ฌธ ์นดํ…Œ๊ณ ๋ฆฌ๋ช… +- **์„ค๋ช… ํ•œ๊ธ€**: description์€ ํ•œ๊ธ€๋กœ ์ƒ์„ธ ์„ค๋ช… +- **์ผ๊ด€์„ฑ ์œ ์ง€**: ์œ ์‚ฌ ๊ธฐ๋Šฅ์€ ๋™์ผํ•œ ํƒœ๊ทธ๋ช… ์‚ฌ์šฉ + +**์˜ˆ์‹œ:** +```yaml +tags: + - name: Authentication + description: ์ธ์ฆ ๊ด€๋ จ API (๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ, ํšŒ์›๊ฐ€์ž…) + - name: Profile + description: ํ”„๋กœํ•„ ๊ด€๋ จ API (์กฐํšŒ, ์ˆ˜์ •, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ) + - name: Event Creation + description: ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ +``` + +--- + +## 5. ์—”๋“œํฌ์ธํŠธ ์ •์˜ + +### 5.1 ์—”๋“œํฌ์ธํŠธ ๊ฒฝ๋กœ ๊ทœ์น™ + +**๊ฒฝ๋กœ ํŒจํ„ด:** +``` +/{resource} +/{resource}/{id} +/{resource}/{id}/{sub-resource} +``` + +**์ค‘์š”: `/api` prefix ์‚ฌ์šฉ ๊ธˆ์ง€** +- โŒ ์ž˜๋ชป๋œ ์˜ˆ: `/api/users/register` +- โœ… ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: `/users/register` + +API Gateway ๋˜๋Š” ์„œ๋ฒ„ URL์—์„œ ์„œ๋น„์Šค ๊ตฌ๋ถ„์ด ์ด๋ฃจ์–ด์ง€๋ฏ€๋กœ, ์—”๋“œํฌ์ธํŠธ ๊ฒฝ๋กœ์— `/api`๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + +### 5.2 ๊ณตํ†ต ์—”๋“œํฌ์ธํŠธ ๊ตฌ์กฐ +```yaml +paths: + /{resource}: + {http-method}: + tags: + - {Tag Name} + summary: {์งง์€ ํ•œ๊ธ€ ์„ค๋ช…} + description: | + {์ƒ์„ธ ์„ค๋ช…} + + **์œ ์ €์Šคํ† ๋ฆฌ:** {UFR ์ฝ”๋“œ} + + **์ฃผ์š” ๊ธฐ๋Šฅ:** + - {๊ธฐ๋Šฅ 1} + - {๊ธฐ๋Šฅ 2} + + **์ฒ˜๋ฆฌ ํ๋ฆ„:** (๋ณต์žกํ•œ ๋กœ์ง์ธ ๊ฒฝ์šฐ) + 1. {๋‹จ๊ณ„ 1} + 2. {๋‹จ๊ณ„ 2} + + **๋ณด์•ˆ:** (๋ณด์•ˆ ๊ด€๋ จ ์—”๋“œํฌ์ธํŠธ์ธ ๊ฒฝ์šฐ) + - {๋ณด์•ˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜} + operationId: {camelCase ๋ฉ”์„œ๋“œ๋ช…} + x-user-story: {UFR ์ฝ”๋“œ} + x-controller: {ControllerClass}.{methodName} + security: + - BearerAuth: [] + parameters: + - $ref: '#/components/parameters/{ParameterName}' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/{RequestSchema}' + examples: + {exampleName}: + summary: {์˜ˆ์‹œ ์„ค๋ช…} + value: {...} + responses: + '{statusCode}': + description: {์‘๋‹ต ์„ค๋ช…} + content: + application/json: + schema: + $ref: '#/components/schemas/{ResponseSchema}' + examples: + {exampleName}: + summary: {์˜ˆ์‹œ ์„ค๋ช…} + value: {...} +``` + +### 5.3 ํ•„์ˆ˜ ํ•ญ๋ชฉ +- `tags`: 1๊ฐœ ์ด์ƒ์˜ ํƒœ๊ทธ ์ง€์ • +- `summary`: ํ•œ๊ธ€๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ (10์ž ์ด๋‚ด ๊ถŒ์žฅ) +- `description`: ๋งˆํฌ๋‹ค์šด ํ˜•์‹์˜ ์ƒ์„ธ ์„ค๋ช… + - ์œ ์ €์Šคํ† ๋ฆฌ ์ฝ”๋“œ ๋ช…์‹œ + - ์ฃผ์š” ๊ธฐ๋Šฅ bullet points + - ๋ณต์žกํ•œ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ ํ๋ฆ„ ์ˆœ์„œ ์ž‘์„ฑ + - ๋ณด์•ˆ ๊ด€๋ จ ๋‚ด์šฉ (ํ•ด๋‹น ์‹œ) +- `operationId`: camelCase ๋ฉ”์„œ๋“œ๋ช… (์˜ˆ: `getUserProfile`, `createEvent`) +- `x-user-story`: UFR ์ฝ”๋“œ (์˜ˆ: `UFR-USER-010`) +- `x-controller`: ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค์™€ ๋ฉ”์„œ๋“œ (์˜ˆ: `UserController.getProfile`) + +### 5.4 operationId ๋ช…๋ช… ๊ทœ์น™ +``` +{๋™์‚ฌ}{๋ช…์‚ฌ} +``` + +**๋™์‚ฌ ๋ชฉ๋ก:** +- `get`: ์กฐํšŒ +- `list`: ๋ชฉ๋ก ์กฐํšŒ +- `create`: ์ƒ์„ฑ +- `update`: ์ˆ˜์ • +- `delete`: ์‚ญ์ œ +- `register`: ๋“ฑ๋ก +- `login`: ๋กœ๊ทธ์ธ +- `logout`: ๋กœ๊ทธ์•„์›ƒ +- `select`: ์„ ํƒ +- `request`: ์š”์ฒญ +- `publish`: ๋ฐฐํฌ +- `end`: ์ข…๋ฃŒ + +**์˜ˆ์‹œ:** +- `getUser`, `listEvents`, `createEvent` +- `updateProfile`, `deleteEvent` +- `registerUser`, `loginUser`, `logoutUser` +- `selectRecommendation`, `publishEvent` + +--- + +## 6. ์‘๋‹ต ๊ตฌ์กฐ + +### 6.1 ์„ฑ๊ณต ์‘๋‹ต (Success Response) + +**์›์น™: ์ง์ ‘ ์‘๋‹ต (Direct Response)** +```yaml +responses: + '200': + description: {์ž‘์—…} ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/{ResponseSchema}' +``` + +**์‘๋‹ต ์Šคํ‚ค๋งˆ ์˜ˆ์‹œ:** +```yaml +UserProfileResponse: + type: object + required: + - userId + - userName + - email + properties: + userId: + type: integer + format: int64 + description: ์‚ฌ์šฉ์ž ID + example: 123 + userName: + type: string + description: ์‚ฌ์šฉ์ž ์ด๋ฆ„ + example: ํ™๊ธธ๋™ + email: + type: string + format: email + description: ์ด๋ฉ”์ผ ์ฃผ์†Œ + example: hong@example.com +``` + +**์˜ˆ์™ธ: Wrapper๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ (๋ฉ”์‹œ์ง€ ์ „๋‹ฌ ํ•„์š” ์‹œ)** +```yaml +LogoutResponse: + type: object + required: + - success + - message + properties: + success: + type: boolean + description: ์„ฑ๊ณต ์—ฌ๋ถ€ + example: true + message: + type: string + description: ์‘๋‹ต ๋ฉ”์‹œ์ง€ + example: ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค +``` + +### 6.2 ํŽ˜์ด์ง• ์‘๋‹ต (Pagination Response) +```yaml +{Resource}ListResponse: + type: object + required: + - content + - page + properties: + content: + type: array + items: + $ref: '#/components/schemas/{ResourceSummary}' + page: + $ref: '#/components/schemas/PageInfo' + +PageInfo: + type: object + required: + - page + - size + - totalElements + - totalPages + properties: + page: + type: integer + description: ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ + example: 0 + size: + type: integer + description: ํŽ˜์ด์ง€ ํฌ๊ธฐ + example: 20 + totalElements: + type: integer + description: ์ „์ฒด ์š”์†Œ ๊ฐœ์ˆ˜ + example: 45 + totalPages: + type: integer + description: ์ „์ฒด ํŽ˜์ด์ง€ ๊ฐœ์ˆ˜ + example: 3 +``` + +--- + +## 7. ์—๋Ÿฌ ์‘๋‹ต ๊ตฌ์กฐ + +### 7.1 ํ‘œ์ค€ ์—๋Ÿฌ ์‘๋‹ต ์Šคํ‚ค๋งˆ +```yaml +ErrorResponse: + type: object + required: + - code + - message + - timestamp + properties: + code: + type: string + description: ์—๋Ÿฌ ์ฝ”๋“œ + example: USER_001 + message: + type: string + description: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + example: ์ด๋ฏธ ๊ฐ€์ž…๋œ ์ „ํ™”๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค + timestamp: + type: string + format: date-time + description: ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ๊ฐ + example: 2025-10-22T10:30:00Z + details: + type: array + description: ์ƒ์„ธ ์—๋Ÿฌ ์ •๋ณด (์„ ํƒ ์‚ฌํ•ญ) + items: + type: string + example: ["ํ•„๋“œ๋ช…: ํ•„์ˆ˜ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค"] +``` + +**ํ•„์ˆ˜ ํ•„๋“œ:** +- `code`: ์—๋Ÿฌ ์ฝ”๋“œ (์„œ๋น„์Šค๋ณ„ ๊ณ ์œ  ์ฝ”๋“œ) +- `message`: ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘œ์‹œํ•  ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ (ํ•œ๊ธ€) +- `timestamp`: ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ๊ฐ (ISO 8601 ํ˜•์‹) + +**์„ ํƒ ํ•„๋“œ:** +- `details`: ์ƒ์„ธ ์—๋Ÿฌ ์ •๋ณด ๋ฐฐ์—ด (validation ์—๋Ÿฌ ๋“ฑ) + +### 7.2 ์—๋Ÿฌ ์ฝ”๋“œ ๋ช…๋ช… ๊ทœ์น™ +``` +{SERVICE}_{NUMBER} +``` + +**์„œ๋น„์Šค ์•ฝ์–ด:** +- `USER`: User Service +- `EVENT`: Event Service +- `CONT`: Content Service +- `AI`: AI Service +- `PART`: Participation Service +- `DIST`: Distribution Service +- `ANAL`: Analytics Service +- `AUTH`: ์ธ์ฆ ๊ด€๋ จ (๊ณตํ†ต) +- `VALIDATION_ERROR`: ์ž…๋ ฅ ๊ฒ€์ฆ ์˜ค๋ฅ˜ (๊ณตํ†ต) + +**์˜ˆ์‹œ:** +- `USER_001`: ์ค‘๋ณต ์‚ฌ์šฉ์ž +- `USER_002`: ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ์‹คํŒจ +- `AUTH_001`: ์ธ์ฆ ์‹คํŒจ +- `AUTH_002`: ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ +- `VALIDATION_ERROR`: ์ž…๋ ฅ ๊ฒ€์ฆ ์˜ค๋ฅ˜ + +### 7.3 ๊ณตํ†ต ์—๋Ÿฌ ์‘๋‹ต ์ •์˜ +```yaml +components: + responses: + BadRequest: + description: ์ž˜๋ชป๋œ ์š”์ฒญ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + validationError: + summary: ์ž…๋ ฅ ๊ฒ€์ฆ ์˜ค๋ฅ˜ + value: + code: VALIDATION_ERROR + message: ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + details: + - "ํ•„๋“œ๋ช…: ํ•„์ˆ˜ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค" + + Unauthorized: + description: ์ธ์ฆ ์‹คํŒจ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + authFailed: + summary: ์ธ์ฆ ์‹คํŒจ + value: + code: AUTH_001 + message: ์ธ์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + + Forbidden: + description: ๊ถŒํ•œ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + forbidden: + summary: ๊ถŒํ•œ ์—†์Œ + value: + code: AUTH_003 + message: ํ•ด๋‹น ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + + NotFound: + description: ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: ๋ฆฌ์†Œ์Šค ์—†์Œ + value: + code: NOT_FOUND + message: ์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + + InternalServerError: + description: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + serverError: + summary: ์„œ๋ฒ„ ์˜ค๋ฅ˜ + value: + code: INTERNAL_SERVER_ERROR + message: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z +``` + +### 7.4 ์—”๋“œํฌ์ธํŠธ๋ณ„ ์—๋Ÿฌ ์‘๋‹ต ์ ์šฉ +```yaml +responses: + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' +``` + +**ํŠน์ˆ˜ ์—๋Ÿฌ (๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์—๋Ÿฌ):** +```yaml +'409': + description: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ถฉ๋Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + duplicateUser: + summary: ์ค‘๋ณต ์‚ฌ์šฉ์ž + value: + code: USER_001 + message: ์ด๋ฏธ ๊ฐ€์ž…๋œ ์ „ํ™”๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z +``` + +--- + +## 8. ์Šคํ‚ค๋งˆ ์ •์˜ + +### 8.1 ์Šคํ‚ค๋งˆ ๋ช…๋ช… ๊ทœ์น™ + +**Request ์Šคํ‚ค๋งˆ:** +``` +{Action}{Resource}Request +``` +์˜ˆ: `RegisterRequest`, `LoginRequest`, `CreateEventRequest` + +**Response ์Šคํ‚ค๋งˆ:** +``` +{Resource}{Type}Response +``` +์˜ˆ: `UserProfileResponse`, `EventListResponse`, `EventDetailResponse` + +**๊ณตํ†ต ๋ชจ๋ธ:** +``` +{Resource}{Type} +``` +์˜ˆ: `EventSummary`, `GeneratedImage`, `PageInfo` + +### 8.2 ์Šคํ‚ค๋งˆ ์ž‘์„ฑ ์›์น™ + +**ํ•„์ˆ˜ ํ•ญ๋ชฉ:** +- `type`: ๊ฐ์ฒด ํƒ€์ž… (object, array, string ๋“ฑ) +- `required`: ํ•„์ˆ˜ ํ•„๋“œ ๋ชฉ๋ก +- `properties`: ๊ฐ ํ•„๋“œ ์ •์˜ + - `type`: ํ•„๋“œ ํƒ€์ž… + - `description`: ํ•„๋“œ ์„ค๋ช… (ํ•œ๊ธ€) + - `example`: ์˜ˆ์‹œ ๊ฐ’ + +**์„ ํƒ ํ•ญ๋ชฉ:** +- `format`: ํŠน์ˆ˜ ํ˜•์‹ (date, date-time, email, uri, uuid, int64 ๋“ฑ) +- `pattern`: ์ •๊ทœ์‹ ํŒจํ„ด (์ „ํ™”๋ฒˆํ˜ธ, ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๋“ฑ) +- `minLength`, `maxLength`: ๋ฌธ์ž์—ด ๊ธธ์ด ์ œํ•œ +- `minimum`, `maximum`: ์ˆซ์ž ๋ฒ”์œ„ ์ œํ•œ +- `enum`: ํ—ˆ์šฉ ๊ฐ’ ๋ชฉ๋ก + +**์˜ˆ์‹œ:** +```yaml +RegisterRequest: + type: object + required: + - name + - phoneNumber + - email + - password + properties: + name: + type: string + minLength: 2 + maxLength: 50 + description: ์‚ฌ์šฉ์ž ์ด๋ฆ„ (2์ž ์ด์ƒ, ํ•œ๊ธ€/์˜๋ฌธ) + example: ํ™๊ธธ๋™ + phoneNumber: + type: string + pattern: '^010\d{8}$' + description: ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ (010XXXXXXXX) + example: "01012345678" + email: + type: string + format: email + maxLength: 100 + description: ์ด๋ฉ”์ผ ์ฃผ์†Œ + example: hong@example.com + password: + type: string + minLength: 8 + maxLength: 100 + description: ๋น„๋ฐ€๋ฒˆํ˜ธ (8์ž ์ด์ƒ, ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ) + example: "Password123!" +``` + +### 8.3 ๋‚ ์งœ/์‹œ๊ฐ„ ํ˜•์‹ + +**๋‚ ์งœ:** `format: date`, ํ˜•์‹ `YYYY-MM-DD` +```yaml +startDate: + type: string + format: date + description: ์‹œ์ž‘์ผ + example: "2025-03-01" +``` + +**๋‚ ์งœ/์‹œ๊ฐ„:** `format: date-time`, ํ˜•์‹ `ISO 8601` +```yaml +createdAt: + type: string + format: date-time + description: ์ƒ์„ฑ์ผ์‹œ + example: 2025-10-22T10:30:00Z +``` + +### 8.4 ID ํ˜•์‹ + +**UUID:** +```yaml +eventId: + type: string + format: uuid + description: ์ด๋ฒคํŠธ ID + example: "550e8400-e29b-41d4-a716-446655440000" +``` + +**์ •์ˆ˜ ID:** +```yaml +userId: + type: integer + format: int64 + description: ์‚ฌ์šฉ์ž ID + example: 123 +``` + +--- + +## 9. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ฃผ์„ + +### 9.1 ํ•„์ˆ˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ +```yaml +x-user-story: {UFR ์ฝ”๋“œ} +x-controller: {ControllerClass}.{methodName} +``` + +**x-user-story:** +- ์œ ์ €์Šคํ† ๋ฆฌ ์ฝ”๋“œ ๋ช…์‹œ +- ์—ฌ๋Ÿฌ ์œ ์ €์Šคํ† ๋ฆฌ์™€ ๊ด€๋ จ๋œ ๊ฒฝ์šฐ ์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„ +- ์˜ˆ: `UFR-USER-010`, `UFR-EVENT-010, UFR-EVENT-070` + +**x-controller:** +- ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค์™€ ๋ฉ”์„œ๋“œ ๋งคํ•‘ +- ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ ์‹œ ์ฐธ์กฐ +- ์˜ˆ: `UserController.registerUser`, `EventController.getEvents` + +### 9.2 ์„ ํƒ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ (ํ•„์š” ์‹œ) +```yaml +x-internal: true # ๋‚ด๋ถ€ API ํ‘œ์‹œ +x-async: true # ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ํ‘œ์‹œ +``` + +--- + +## 10. ๊ธฐ์ˆ  ๋ช…์„ธ ์„น์…˜ + +### 10.1 x-technical-specifications + +**๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์„œ๋น„์Šค (AI, Content ๋“ฑ):** +```yaml +x-technical-specifications: + async-processing: + message-queue: Kafka + topics: + request: ai.recommendation.request + response: ai.recommendation.response + job-tracking: Redis (TTL 24h) + timeout: 300s + + resilience: + circuit-breaker: + failure-threshold: 5 + timeout: 10s + half-open-requests: 3 + retry: + max-attempts: 3 + backoff: exponential + initial-interval: 1s + max-interval: 10s + fallback: + strategy: cached-result + + caching: + provider: Redis + ttl: 7d + key-pattern: "content:event:{eventDraftId}" + + external-apis: + - name: Claude API + endpoint: https://api.anthropic.com/v1/messages + timeout: 60s + circuit-breaker: true + - name: GPT-4 API + endpoint: https://api.openai.com/v1/chat/completions + timeout: 60s + circuit-breaker: true +``` + +**๋™๊ธฐ ์ฒ˜๋ฆฌ ์„œ๋น„์Šค:** +```yaml +x-technical-specifications: + database: + type: PostgreSQL + connection-pool: + min: 10 + max: 50 + timeout: 30s + + caching: + provider: Redis + ttl: 30m + key-pattern: "user:{userId}" + + security: + authentication: JWT Bearer + password-hashing: bcrypt + encryption: + algorithm: AES-256-GCM + fields: [businessNumber] +``` + +### 10.2 ์ ์šฉ ๊ธฐ์ค€ + +**ํ•„์ˆ˜ ํฌํ•จ ์„œ๋น„์Šค:** +- Content Service: ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ, Kafka, ์™ธ๋ถ€ API ํ†ตํ•ฉ +- AI Service: ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ, Kafka, Claude/GPT ํ†ตํ•ฉ + +**์„ ํƒ ํฌํ•จ ์„œ๋น„์Šค:** +- User Service: ๋ณด์•ˆ ๊ด€๋ จ ๋ช…์„ธ +- Event Service: ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ํŒจํ„ด +- Participation Service: ๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ ๋Œ€๋น„ ์บ์‹ฑ + +--- + +## 11. ์˜ˆ์ œ ์ž‘์„ฑ + +### 11.1 Request/Response ์˜ˆ์ œ ์›์น™ + +**๋ชจ๋“  requestBody์™€ ์ฃผ์š” response์— ์˜ˆ์ œ ํ•„์ˆ˜:** +```yaml +requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + examples: + restaurant: + summary: ์Œ์‹์  ํšŒ์›๊ฐ€์ž… ์˜ˆ์‹œ + value: + name: ํ™๊ธธ๋™ + phoneNumber: "01012345678" + email: hong@example.com + password: "Password123!" + storeName: ๋ง›์žˆ๋Š”์ง‘ + industry: ์Œ์‹์  + cafe: + summary: ์นดํŽ˜ ํšŒ์›๊ฐ€์ž… ์˜ˆ์‹œ + value: + name: ๊น€์ฒ ์ˆ˜ + phoneNumber: "01087654321" + email: kim@example.com + password: "SecurePass456!" + storeName: ์•„๋ฉ”๋ฆฌ์นด๋…ธ ์นดํŽ˜ + industry: ์นดํŽ˜ +``` + +**์„ฑ๊ณต ์‘๋‹ต ์˜ˆ์ œ:** +```yaml +responses: + '200': + description: ํ”„๋กœํ•„ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileResponse' + examples: + success: + summary: ํ”„๋กœํ•„ ์กฐํšŒ ์„ฑ๊ณต ์‘๋‹ต + value: + userId: 123 + userName: ํ™๊ธธ๋™ + phoneNumber: "01012345678" + email: hong@example.com +``` + +**์—๋Ÿฌ ์‘๋‹ต ์˜ˆ์ œ:** +```yaml +responses: + '400': + description: ์ž˜๋ชป๋œ ์š”์ฒญ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + duplicateUser: + summary: ์ค‘๋ณต ์‚ฌ์šฉ์ž + value: + code: USER_001 + message: ์ด๋ฏธ ๊ฐ€์ž…๋œ ์ „ํ™”๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + invalidBusinessNumber: + summary: ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ์‹คํŒจ + value: + code: USER_002 + message: ์œ ํšจํ•˜์ง€ ์•Š์€ ์‚ฌ์—…์ž๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z +``` + +### 11.2 ์˜ˆ์ œ ๋ช…๋ช… ๊ทœ์น™ +- `success`: ์„ฑ๊ณต ์ผ€์ด์Šค +- `{errorType}`: ์—๋Ÿฌ ์ผ€์ด์Šค (์˜ˆ: `duplicateUser`, `validationError`) +- `{scenario}`: ์‹œ๋‚˜๋ฆฌ์˜ค๋ณ„ ์˜ˆ์ œ (์˜ˆ: `restaurant`, `cafe`) + +--- + +## 12. ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +API ๋ช…์„ธ์„œ ์ž‘์„ฑ ์‹œ ์•„๋ž˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ํ™•์ธํ•˜์„ธ์š”: + +### ๊ธฐ๋ณธ ์ •๋ณด +- [ ] OpenAPI ๋ฒ„์ „ 3.0.3 ๋ช…์‹œ +- [ ] info.title์— ์„œ๋น„์Šค๋ช… ํฌํ•จ +- [ ] info.description์— ์ฃผ์š” ๊ธฐ๋Šฅ ๋ชฉ๋ก ํฌํ•จ +- [ ] info.version 1.0.0 +- [ ] contact ์ •๋ณด ํฌํ•จ + +### ์„œ๋ฒ„ ๋ฐ ๋ณด์•ˆ +- [ ] servers์— Local, Dev, Prod ์ •์˜ +- [ ] ํฌํŠธ ๋ฒˆํ˜ธ ์ •ํ™•ํžˆ ํ• ๋‹น +- [ ] components.securitySchemes์— BearerAuth ์ •์˜ +- [ ] ์ธ์ฆ ํ•„์š”ํ•œ ์—”๋“œํฌ์ธํŠธ์— security ์ ์šฉ + +### ์—”๋“œํฌ์ธํŠธ +- [ ] ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ์— tags ์ง€์ • +- [ ] summary์™€ description ์ž‘์„ฑ (ํ•œ๊ธ€) +- [ ] operationId camelCase๋กœ ์ž‘์„ฑ +- [ ] x-user-story UFR ์ฝ”๋“œ ๋ช…์‹œ +- [ ] x-controller ๋งคํ•‘ ์ •๋ณด ํฌํ•จ + +### ์Šคํ‚ค๋งˆ +- [ ] Request/Response ์Šคํ‚ค๋งˆ ๋ช…๋ช… ๊ทœ์น™ ์ค€์ˆ˜ +- [ ] required ํ•„๋“œ ๋ช…์‹œ +- [ ] ๋ชจ๋“  properties์— description๊ณผ example ํฌํ•จ +- [ ] ์ ์ ˆํ•œ format ์‚ฌ์šฉ (date, date-time, email, uuid ๋“ฑ) + +### ์‘๋‹ต ๊ตฌ์กฐ +- [ ] ErrorResponse ํ‘œ์ค€ ์Šคํ‚ค๋งˆ ์‚ฌ์šฉ +- [ ] ๊ณตํ†ต ์—๋Ÿฌ ์‘๋‹ต ($ref) ํ™œ์šฉ +- [ ] ์—๋Ÿฌ ์ฝ”๋“œ ๋ช…๋ช… ๊ทœ์น™ ์ค€์ˆ˜ +- [ ] ํŽ˜์ด์ง• ์‘๋‹ต์— PageInfo ์‚ฌ์šฉ + +### ์˜ˆ์ œ +- [ ] requestBody์— ์ตœ์†Œ 1๊ฐœ ์ด์ƒ ์˜ˆ์ œ +- [ ] ์ฃผ์š” response์— success ์˜ˆ์ œ +- [ ] ์ฃผ์š” ์—๋Ÿฌ ์ผ€์ด์Šค์— ์˜ˆ์ œ + +### ๊ธฐ์ˆ  ๋ช…์„ธ (ํ•ด๋‹น ์‹œ) +- [ ] ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์„œ๋น„์Šค: x-technical-specifications ํฌํ•จ +- [ ] Kafka ํ† ํ”ฝ, Redis ์บ์‹ฑ ์ •๋ณด ๋ช…์‹œ +- [ ] ์™ธ๋ถ€ API ์—ฐ๋™ ์ •๋ณด ํฌํ•จ + +--- + +## 13. ์ฐธ๊ณ  ์ž๋ฃŒ + +### ์„œ๋น„์Šค๋ณ„ API ๋ช…์„ธ์„œ +- User Service API: `/design/backend/api/user-service-api.yaml` +- Event Service API: `/design/backend/api/event-service-api.yaml` +- Content Service API: `/design/backend/api/content-service-api.yaml` +- AI Service API: `/design/backend/api/ai-service-api.yaml` +- Participation Service API: `/design/backend/api/participation-service-api.yaml` +- Distribution Service API: `/design/backend/api/distribution-service-api.yaml` +- Analytics Service API: `/design/backend/api/analytics-service-api.yaml` + +### OpenAPI 3.0.3 ๊ณต์‹ ๋ฌธ์„œ +- https://swagger.io/specification/ + +### ํ”„๋กœ์ ํŠธ ์•„ํ‚คํ…์ฒ˜ +- High-Level Architecture: `/design/high-level-architecture.md` +- Logical Architecture: `/design/backend/logical/` + +--- + +**๋ฌธ์„œ ๋ฒ„์ „:** 1.0.0 +**์ตœ์ข… ์ˆ˜์ •์ผ:** 2025-10-23 +**์ž‘์„ฑ์ž:** Digital Garage Team diff --git a/design/backend/api/ai-service-api.yaml b/design/backend/api/ai-service-api.yaml new file mode 100644 index 0000000..b4ba555 --- /dev/null +++ b/design/backend/api/ai-service-api.yaml @@ -0,0 +1,849 @@ +openapi: 3.0.3 +info: + title: AI Service API + description: | + KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - AI Service + + ## ์„œ๋น„์Šค ๊ฐœ์š” + - Kafka๋ฅผ ํ†ตํ•œ ๋น„๋™๊ธฐ AI ์ถ”์ฒœ ์ฒ˜๋ฆฌ + - Claude API / GPT-4 API ์—ฐ๋™ + - Redis ๊ธฐ๋ฐ˜ ๊ฒฐ๊ณผ ์บ์‹ฑ (TTL 24์‹œ๊ฐ„) + + ## ์ฒ˜๋ฆฌ ํ๋ฆ„ + 1. Event Service๊ฐ€ Kafka Topic์— Job ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ + 2. AI Service๊ฐ€ ๋ฉ”์‹œ์ง€ ๊ตฌ๋… ๋ฐ ์ฒ˜๋ฆฌ + 3. ํŠธ๋ Œ๋“œ ๋ถ„์„ ์ˆ˜ํ–‰ (Claude/GPT-4 API) + 4. 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ์ถ”์ฒœ์•ˆ ์ƒ์„ฑ + 5. ๊ฒฐ๊ณผ๋ฅผ Redis์— ์ €์žฅ (TTL 24์‹œ๊ฐ„) + 6. Job ์ƒํƒœ๋ฅผ Redis์— ์—…๋ฐ์ดํŠธ + + ## ์™ธ๋ถ€ API ํ†ตํ•ฉ + - **Claude API / GPT-4 API**: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ + - **Circuit Breaker**: 5๋ถ„ ํƒ€์ž„์•„์›ƒ, Fallback to ์บ์‹œ + - **Retry**: ์ตœ๋Œ€ 3ํšŒ, Exponential Backoff + + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8083 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/ai/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/ai/v1 + description: Production Server + +tags: + - name: Health Check + description: ์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ + - name: Internal API + description: ๋‚ด๋ถ€ ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์šฉ API + - name: Kafka Consumer + description: ๋น„๋™๊ธฐ ์ž‘์—… ์ฒ˜๋ฆฌ (๋ฌธ์„œํ™”๋งŒ) + +paths: + /health: + get: + tags: + - Health Check + summary: ์„œ๋น„์Šค ํ—ฌ์Šค์ฒดํฌ + description: AI Service ์ƒํƒœ ๋ฐ ์™ธ๋ถ€ ์—ฐ๋™ ํ™•์ธ + operationId: healthCheck + x-user-story: System + x-controller: HealthController + responses: + '200': + description: ์„œ๋น„์Šค ์ •์ƒ + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheckResponse' + example: + status: UP + timestamp: "2025-10-23T10:30:00Z" + services: + kafka: UP + redis: UP + claude_api: UP + gpt4_api: UP + circuit_breaker: CLOSED + + /internal/jobs/{jobId}/status: + get: + tags: + - Internal API + summary: ์ž‘์—… ์ƒํƒœ ์กฐํšŒ + description: Redis์— ์ €์žฅ๋œ AI ์ถ”์ฒœ ์ž‘์—… ์ƒํƒœ ์กฐํšŒ (Event Service์—์„œ ํ˜ธ์ถœ) + operationId: getJobStatus + x-user-story: UFR-AI-010 + x-controller: InternalJobController + parameters: + - name: jobId + in: path + required: true + schema: + type: string + description: Job ID + example: "job-ai-evt001-20251023103000" + responses: + '200': + description: ์ž‘์—… ์ƒํƒœ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/JobStatusResponse' + examples: + processing: + summary: ์ฒ˜๋ฆฌ ์ค‘ + value: + jobId: "job-ai-evt001-20251023103000" + status: "PROCESSING" + progress: 50 + message: "AI ์ถ”์ฒœ ์ƒ์„ฑ ์ค‘" + createdAt: "2025-10-23T10:30:00Z" + startedAt: "2025-10-23T10:30:05Z" + completed: + summary: ์™„๋ฃŒ + value: + jobId: "job-ai-evt001-20251023103000" + status: "COMPLETED" + progress: 100 + message: "AI ์ถ”์ฒœ ์™„๋ฃŒ" + createdAt: "2025-10-23T10:30:00Z" + startedAt: "2025-10-23T10:30:05Z" + completedAt: "2025-10-23T10:35:00Z" + processingTimeMs: 295000 + failed: + summary: ์‹คํŒจ + value: + jobId: "job-ai-evt001-20251023103000" + status: "FAILED" + progress: 0 + message: "Claude API timeout" + errorMessage: "Claude API timeout after 5 minutes" + createdAt: "2025-10-23T10:30:00Z" + startedAt: "2025-10-23T10:30:05Z" + failedAt: "2025-10-23T10:35:05Z" + retryCount: 3 + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /internal/recommendations/{eventId}: + get: + tags: + - Internal API + summary: AI ์ถ”์ฒœ ๊ฒฐ๊ณผ ์กฐํšŒ + description: Redis์— ์บ์‹œ๋œ AI ์ถ”์ฒœ ๊ฒฐ๊ณผ ์กฐํšŒ (Event Service์—์„œ ํ˜ธ์ถœ) + operationId: getRecommendation + x-user-story: UFR-AI-010 + x-controller: InternalRecommendationController + parameters: + - name: eventId + in: path + required: true + schema: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt-001" + responses: + '200': + description: ์ถ”์ฒœ ๊ฒฐ๊ณผ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/AIRecommendationResult' + example: + eventId: "evt-001" + trendAnalysis: + industryTrends: + - keyword: "ํ”„๋ฆฌ๋ฏธ์—„ ๋””์ €ํŠธ" + relevance: 0.85 + description: "๊ณ ๊ธ‰ ๋””์ €ํŠธ ์นดํŽ˜ ํŠธ๋ Œ๋“œ ์ฆ๊ฐ€" + regionalTrends: + - keyword: "ํ•ซํ”Œ๋ ˆ์ด์Šค" + relevance: 0.78 + description: "๊ฐ•๋‚จ ์‹ ๋…ผํ˜„์—ญ ์ฃผ๋ณ€ ์œ ๋™์ธ๊ตฌ ์ฆ๊ฐ€" + seasonalTrends: + - keyword: "๊ฐ€์„ ์‹œ์ฆŒ" + relevance: 0.92 + description: "๊ฐ€์„ ์‹œ์ฆŒ ํ•œ์ • ๋ฉ”๋‰ด ์„ ํ˜ธ๋„ ์ฆ๊ฐ€" + recommendations: + - optionNumber: 1 + concept: "ํ”„๋ฆฌ๋ฏธ์—„ ๊ฒฝํ—˜ํ˜•" + title: "๊ฐ€์„ ํ•œ์ • ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋””์ €ํŠธ ํŽ˜์–ด๋ง ์ด๋ฒคํŠธ" + description: "๊ฐ€์„ ์ œ์ฒ  ์žฌ๋ฃŒ๋ฅผ ํ™œ์šฉํ•œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋””์ €ํŠธ์™€ ์Œ๋ฃŒ ํŽ˜์–ด๋ง ์ฒดํ—˜" + targetAudience: "20-30๋Œ€ ์—ฌ์„ฑ, SNS ํ™œ๋™์ ์ธ ๊ณ ๊ฐ" + duration: + recommendedDays: 14 + recommendedPeriod: "10์›” ์ค‘์ˆœ ~ 11์›” ์ดˆ" + mechanics: + type: "EXPERIENCE" + details: "๋””์ €ํŠธ+์Œ๋ฃŒ ํŽ˜์–ด๋ง ์„ธํŠธ ์ฃผ๋ฌธ ์‹œ ์ธ์Šคํƒ€๊ทธ๋žจ ์—…๋กœ๋“œ ๊ณ ๊ฐ์—๊ฒŒ ๋‹ค์Œ ๋ฐฉ๋ฌธ ์‹œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ 10% ํ• ์ธ๊ถŒ ์ œ๊ณต" + promotionChannels: + - "Instagram" + - "์นด์นด์˜คํ†ก ์ฑ„๋„" + - "๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค" + estimatedCost: + min: 300000 + max: 500000 + breakdown: + material: 200000 + promotion: 150000 + discount: 150000 + expectedMetrics: + newCustomers: + min: 50 + max: 80 + repeatVisits: + min: 30 + max: 50 + revenueIncrease: + min: 15.0 + max: 25.0 + roi: + min: 120.0 + max: 180.0 + socialEngagement: + estimatedPosts: 100 + estimatedReach: 5000 + differentiator: "ํ”„๋ฆฌ๋ฏธ์—„ ๊ฒฝํ—˜ ์ œ๊ณต์œผ๋กœ ๊ณ ๊ฐ ๋งŒ์กฑ๋„์™€ SNS ๋ฐ”์ด๋Ÿด ํšจ๊ณผ ๊ทน๋Œ€ํ™”" + generatedAt: "2025-10-23T10:35:00Z" + expiresAt: "2025-10-24T10:35:00Z" + aiProvider: "CLAUDE" + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + +components: + schemas: + # ==================== Health Check ==================== + HealthCheckResponse: + type: object + description: ์„œ๋น„์Šค ํ—ฌ์Šค์ฒดํฌ ์‘๋‹ต + required: + - status + - timestamp + - services + properties: + status: + type: string + enum: [UP, DOWN, DEGRADED] + description: ์ „์ฒด ์„œ๋น„์Šค ์ƒํƒœ + example: UP + timestamp: + type: string + format: date-time + description: ์ฒดํฌ ์‹œ๊ฐ + example: "2025-10-23T10:30:00Z" + services: + type: object + description: ๊ฐœ๋ณ„ ์„œ๋น„์Šค ์ƒํƒœ + required: + - kafka + - redis + - claude_api + - circuit_breaker + properties: + kafka: + type: string + enum: [UP, DOWN] + description: Kafka ์—ฐ๊ฒฐ ์ƒํƒœ + example: UP + redis: + type: string + enum: [UP, DOWN] + description: Redis ์—ฐ๊ฒฐ ์ƒํƒœ + example: UP + claude_api: + type: string + enum: [UP, DOWN, CIRCUIT_OPEN] + description: Claude API ์ƒํƒœ + example: UP + gpt4_api: + type: string + enum: [UP, DOWN, CIRCUIT_OPEN] + description: GPT-4 API ์ƒํƒœ (์„ ํƒ) + example: UP + circuit_breaker: + type: string + enum: [CLOSED, OPEN, HALF_OPEN] + description: Circuit Breaker ์ƒํƒœ + example: CLOSED + + # ==================== Kafka Job Message (๋ฌธ์„œํ™”๋งŒ) ==================== + KafkaAIJobMessage: + type: object + description: | + **Kafka Topic**: `ai-event-generation-job` + **Consumer Group**: `ai-service-consumers` + **์ฒ˜๋ฆฌ ๋ฐฉ์‹**: ๋น„๋™๊ธฐ + **์ตœ๋Œ€ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„**: 5๋ถ„ + + AI ์ด๋ฒคํŠธ ์ƒ์„ฑ ์š”์ฒญ ๋ฉ”์‹œ์ง€ + required: + - jobId + - eventId + - objective + - industry + - region + properties: + jobId: + type: string + description: Job ๊ณ ์œ  ID + example: "job-ai-evt001-20251023103000" + eventId: + type: string + description: ์ด๋ฒคํŠธ ID (Event Service์—์„œ ์ƒ์„ฑ) + example: "evt-001" + objective: + type: string + description: ์ด๋ฒคํŠธ ๋ชฉ์  + enum: + - "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜" + - "์žฌ๋ฐฉ๋ฌธ ์œ ๋„" + - "๋งค์ถœ ์ฆ๋Œ€" + - "๋ธŒ๋žœ๋“œ ์ธ์ง€๋„ ํ–ฅ์ƒ" + example: "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜" + industry: + type: string + description: ์—…์ข… + example: "์Œ์‹์ " + region: + type: string + description: ์ง€์—ญ (์‹œ/๊ตฌ/๋™) + example: "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ" + storeName: + type: string + description: ๋งค์žฅ๋ช… (์„ ํƒ) + example: "๋ง›์žˆ๋Š” ๊ณ ๊นƒ์ง‘" + targetAudience: + type: string + description: ๋ชฉํ‘œ ๊ณ ๊ฐ์ธต (์„ ํƒ) + example: "20-30๋Œ€ ์—ฌ์„ฑ" + budget: + type: integer + description: ์˜ˆ์‚ฐ (์›) (์„ ํƒ) + example: 500000 + requestedAt: + type: string + format: date-time + description: ์š”์ฒญ ์‹œ๊ฐ + example: "2025-10-23T10:30:00Z" + + # ==================== AI Recommendation Result ==================== + AIRecommendationResult: + type: object + description: | + **Redis Key**: `ai:recommendation:{eventId}` + **TTL**: 86400์ดˆ (24์‹œ๊ฐ„) + + AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ๊ฒฐ๊ณผ + required: + - eventId + - trendAnalysis + - recommendations + - generatedAt + - aiProvider + properties: + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt-001" + trendAnalysis: + $ref: '#/components/schemas/TrendAnalysis' + recommendations: + type: array + description: ์ถ”์ฒœ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ (3๊ฐœ) + minItems: 3 + maxItems: 3 + items: + $ref: '#/components/schemas/EventRecommendation' + generatedAt: + type: string + format: date-time + description: ์ƒ์„ฑ ์‹œ๊ฐ + example: "2025-10-23T10:35:00Z" + expiresAt: + type: string + format: date-time + description: ์บ์‹œ ๋งŒ๋ฃŒ ์‹œ๊ฐ (์ƒ์„ฑ ์‹œ๊ฐ + 24์‹œ๊ฐ„) + example: "2025-10-24T10:35:00Z" + aiProvider: + type: string + enum: [CLAUDE, GPT4] + description: ์‚ฌ์šฉ๋œ AI ์ œ๊ณต์ž + example: "CLAUDE" + + TrendAnalysis: + type: object + description: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ (์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ) + required: + - industryTrends + - regionalTrends + - seasonalTrends + properties: + industryTrends: + type: array + description: ์—…์ข… ํŠธ๋ Œ๋“œ ํ‚ค์›Œ๋“œ (์ตœ๋Œ€ 5๊ฐœ) + maxItems: 5 + items: + type: object + required: + - keyword + - relevance + - description + properties: + keyword: + type: string + description: ํŠธ๋ Œ๋“œ ํ‚ค์›Œ๋“œ + example: "ํ”„๋ฆฌ๋ฏธ์—„ ๋””์ €ํŠธ" + relevance: + type: number + format: float + minimum: 0 + maximum: 1 + description: ์—ฐ๊ด€๋„ (0-1) + example: 0.85 + description: + type: string + description: ํŠธ๋ Œ๋“œ ์„ค๋ช… + example: "๊ณ ๊ธ‰ ๋””์ €ํŠธ ์นดํŽ˜ ํŠธ๋ Œ๋“œ ์ฆ๊ฐ€" + regionalTrends: + type: array + description: ์ง€์—ญ ํŠธ๋ Œ๋“œ ํ‚ค์›Œ๋“œ (์ตœ๋Œ€ 5๊ฐœ) + maxItems: 5 + items: + type: object + required: + - keyword + - relevance + - description + properties: + keyword: + type: string + example: "ํ•ซํ”Œ๋ ˆ์ด์Šค" + relevance: + type: number + format: float + minimum: 0 + maximum: 1 + example: 0.78 + description: + type: string + example: "๊ฐ•๋‚จ ์‹ ๋…ผํ˜„์—ญ ์ฃผ๋ณ€ ์œ ๋™์ธ๊ตฌ ์ฆ๊ฐ€" + seasonalTrends: + type: array + description: ์‹œ์ฆŒ ํŠธ๋ Œ๋“œ ํ‚ค์›Œ๋“œ (์ตœ๋Œ€ 5๊ฐœ) + maxItems: 5 + items: + type: object + required: + - keyword + - relevance + - description + properties: + keyword: + type: string + example: "๊ฐ€์„ ์‹œ์ฆŒ" + relevance: + type: number + format: float + minimum: 0 + maximum: 1 + example: 0.92 + description: + type: string + example: "๊ฐ€์„ ์‹œ์ฆŒ ํ•œ์ • ๋ฉ”๋‰ด ์„ ํ˜ธ๋„ ์ฆ๊ฐ€" + + EventRecommendation: + type: object + description: ์ด๋ฒคํŠธ ์ถ”์ฒœ์•ˆ (์ฐจ๋ณ„ํ™”๋œ 3๊ฐ€์ง€ ์˜ต์…˜) + required: + - optionNumber + - concept + - title + - description + - targetAudience + - duration + - mechanics + - promotionChannels + - estimatedCost + - expectedMetrics + - differentiator + properties: + optionNumber: + type: integer + description: ์˜ต์…˜ ๋ฒˆํ˜ธ (1-3) + minimum: 1 + maximum: 3 + example: 1 + concept: + type: string + description: ์ด๋ฒคํŠธ ์ปจ์…‰ + example: "ํ”„๋ฆฌ๋ฏธ์—„ ๊ฒฝํ—˜ํ˜•" + title: + type: string + description: ์ด๋ฒคํŠธ ์ œ๋ชฉ + maxLength: 100 + example: "๊ฐ€์„ ํ•œ์ • ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋””์ €ํŠธ ํŽ˜์–ด๋ง ์ด๋ฒคํŠธ" + description: + type: string + description: ์ด๋ฒคํŠธ ์„ค๋ช… + maxLength: 500 + example: "๊ฐ€์„ ์ œ์ฒ  ์žฌ๋ฃŒ๋ฅผ ํ™œ์šฉํ•œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋””์ €ํŠธ์™€ ์Œ๋ฃŒ ํŽ˜์–ด๋ง ์ฒดํ—˜" + targetAudience: + type: string + description: ๋ชฉํ‘œ ๊ณ ๊ฐ์ธต + example: "20-30๋Œ€ ์—ฌ์„ฑ, SNS ํ™œ๋™์ ์ธ ๊ณ ๊ฐ" + duration: + type: object + description: ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„ + required: + - recommendedDays + properties: + recommendedDays: + type: integer + description: ๊ถŒ์žฅ ์ง„ํ–‰ ์ผ์ˆ˜ + minimum: 1 + example: 14 + recommendedPeriod: + type: string + description: ๊ถŒ์žฅ ์ง„ํ–‰ ์‹œ๊ธฐ + example: "10์›” ์ค‘์ˆœ ~ 11์›” ์ดˆ" + mechanics: + type: object + description: ์ด๋ฒคํŠธ ๋ฉ”์ปค๋‹ˆ์ฆ˜ + required: + - type + - details + properties: + type: + type: string + enum: [DISCOUNT, GIFT, STAMP, EXPERIENCE, LOTTERY, COMBO] + description: ์ด๋ฒคํŠธ ์œ ํ˜• + example: "EXPERIENCE" + details: + type: string + description: ์ƒ์„ธ ๋ฉ”์ปค๋‹ˆ์ฆ˜ + maxLength: 500 + example: "๋””์ €ํŠธ+์Œ๋ฃŒ ํŽ˜์–ด๋ง ์„ธํŠธ ์ฃผ๋ฌธ ์‹œ ์ธ์Šคํƒ€๊ทธ๋žจ ์—…๋กœ๋“œ ๊ณ ๊ฐ์—๊ฒŒ ๋‹ค์Œ ๋ฐฉ๋ฌธ ์‹œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ 10% ํ• ์ธ๊ถŒ ์ œ๊ณต" + promotionChannels: + type: array + description: ์ถ”์ฒœ ํ™๋ณด ์ฑ„๋„ (์ตœ๋Œ€ 5๊ฐœ) + maxItems: 5 + items: + type: string + example: + - "Instagram" + - "์นด์นด์˜คํ†ก ์ฑ„๋„" + - "๋„ค์ด๋ฒ„ ํ”Œ๋ ˆ์ด์Šค" + estimatedCost: + type: object + description: ์˜ˆ์ƒ ๋น„์šฉ + required: + - min + - max + properties: + min: + type: integer + description: ์ตœ์†Œ ๋น„์šฉ (์›) + minimum: 0 + example: 300000 + max: + type: integer + description: ์ตœ๋Œ€ ๋น„์šฉ (์›) + minimum: 0 + example: 500000 + breakdown: + type: object + description: ๋น„์šฉ ๊ตฌ์„ฑ + properties: + material: + type: integer + description: ์žฌ๋ฃŒ๋น„ (์›) + example: 200000 + promotion: + type: integer + description: ํ™๋ณด๋น„ (์›) + example: 150000 + discount: + type: integer + description: ํ• ์ธ ๋น„์šฉ (์›) + example: 150000 + expectedMetrics: + $ref: '#/components/schemas/ExpectedMetrics' + differentiator: + type: string + description: ๋‹ค๋ฅธ ์˜ต์…˜๊ณผ์˜ ์ฐจ๋ณ„์  + maxLength: 500 + example: "ํ”„๋ฆฌ๋ฏธ์—„ ๊ฒฝํ—˜ ์ œ๊ณต์œผ๋กœ ๊ณ ๊ฐ ๋งŒ์กฑ๋„์™€ SNS ๋ฐ”์ด๋Ÿด ํšจ๊ณผ ๊ทน๋Œ€ํ™”, ๋ธŒ๋žœ๋“œ ์ด๋ฏธ์ง€ ํ–ฅ์ƒ์— ์ง‘์ค‘" + + ExpectedMetrics: + type: object + description: ์˜ˆ์ƒ ์„ฑ๊ณผ ์ง€ํ‘œ + required: + - newCustomers + - revenueIncrease + - roi + properties: + newCustomers: + type: object + description: ์‹ ๊ทœ ๊ณ ๊ฐ ์ˆ˜ + required: + - min + - max + properties: + min: + type: integer + minimum: 0 + example: 50 + max: + type: integer + minimum: 0 + example: 80 + repeatVisits: + type: object + description: ์žฌ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ์ˆ˜ (์„ ํƒ) + properties: + min: + type: integer + minimum: 0 + example: 30 + max: + type: integer + minimum: 0 + example: 50 + revenueIncrease: + type: object + description: ๋งค์ถœ ์ฆ๊ฐ€์œจ (%) + required: + - min + - max + properties: + min: + type: number + format: float + minimum: 0 + example: 15.0 + max: + type: number + format: float + minimum: 0 + example: 25.0 + roi: + type: object + description: ROI - ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  (%) + required: + - min + - max + properties: + min: + type: number + format: float + minimum: 0 + example: 120.0 + max: + type: number + format: float + minimum: 0 + example: 180.0 + socialEngagement: + type: object + description: SNS ์ฐธ์—ฌ๋„ (์„ ํƒ) + properties: + estimatedPosts: + type: integer + description: ์˜ˆ์ƒ ๊ฒŒ์‹œ๋ฌผ ์ˆ˜ + minimum: 0 + example: 100 + estimatedReach: + type: integer + description: ์˜ˆ์ƒ ๋„๋‹ฌ ์ˆ˜ + minimum: 0 + example: 5000 + + # ==================== Job Status ==================== + JobStatusResponse: + type: object + description: | + **Redis Key**: `ai:job:status:{jobId}` + **TTL**: 86400์ดˆ (24์‹œ๊ฐ„) + + ์ž‘์—… ์ƒํƒœ ์‘๋‹ต + required: + - jobId + - status + - progress + - message + - createdAt + properties: + jobId: + type: string + description: Job ID + example: "job-ai-evt001-20251023103000" + status: + type: string + enum: [PENDING, PROCESSING, COMPLETED, FAILED] + description: ์ž‘์—… ์ƒํƒœ + example: "COMPLETED" + progress: + type: integer + minimum: 0 + maximum: 100 + description: ์ง„ํ–‰๋ฅ  (%) + example: 100 + message: + type: string + description: ์ƒํƒœ ๋ฉ”์‹œ์ง€ + example: "AI ์ถ”์ฒœ ์™„๋ฃŒ" + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt-001" + createdAt: + type: string + format: date-time + description: ์ž‘์—… ์ƒ์„ฑ ์‹œ๊ฐ + example: "2025-10-23T10:30:00Z" + startedAt: + type: string + format: date-time + description: ์ž‘์—… ์‹œ์ž‘ ์‹œ๊ฐ + example: "2025-10-23T10:30:05Z" + completedAt: + type: string + format: date-time + description: ์ž‘์—… ์™„๋ฃŒ ์‹œ๊ฐ (์™„๋ฃŒ ์‹œ) + example: "2025-10-23T10:35:00Z" + failedAt: + type: string + format: date-time + description: ์ž‘์—… ์‹คํŒจ ์‹œ๊ฐ (์‹คํŒจ ์‹œ) + example: "2025-10-23T10:35:05Z" + errorMessage: + type: string + description: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ (์‹คํŒจ ์‹œ) + example: "Claude API timeout after 5 minutes" + retryCount: + type: integer + description: ์žฌ์‹œ๋„ ํšŸ์ˆ˜ + minimum: 0 + example: 0 + processingTimeMs: + type: integer + description: ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + minimum: 0 + example: 295000 + + # ==================== Error Response ==================== + ErrorResponse: + type: object + description: ์—๋Ÿฌ ์‘๋‹ต + required: + - code + - message + - timestamp + properties: + code: + type: string + description: ์—๋Ÿฌ ์ฝ”๋“œ + enum: + - AI_SERVICE_ERROR + - JOB_NOT_FOUND + - RECOMMENDATION_NOT_FOUND + - REDIS_ERROR + - KAFKA_ERROR + - CIRCUIT_BREAKER_OPEN + - INTERNAL_ERROR + example: "JOB_NOT_FOUND" + message: + type: string + description: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + example: "์ž‘์—…์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + timestamp: + type: string + format: date-time + description: ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ๊ฐ + example: "2025-10-23T10:30:00Z" + details: + type: object + description: ์ถ”๊ฐ€ ์—๋Ÿฌ ์ƒ์„ธ + additionalProperties: true + example: + jobId: "job-ai-evt001-20251023103000" + + responses: + NotFound: + description: ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "JOB_NOT_FOUND" + message: "์ž‘์—…์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + timestamp: "2025-10-23T10:30:00Z" + + InternalServerError: + description: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: "INTERNAL_ERROR" + message: "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค" + timestamp: "2025-10-23T10:30:00Z" + +# ==================== ๊ธฐ์ˆ  ๊ตฌ์„ฑ ๋ฌธ์„œํ™” ==================== +x-technical-specifications: + circuit-breaker: + claude-api: + failureThreshold: 5 + successThreshold: 2 + timeout: 300000 + resetTimeout: 60000 + fallbackStrategy: CACHED_RECOMMENDATION + gpt4-api: + failureThreshold: 5 + successThreshold: 2 + timeout: 300000 + resetTimeout: 60000 + fallbackStrategy: CACHED_RECOMMENDATION + + redis-cache: + patterns: + recommendation: "ai:recommendation:{eventId}" + jobStatus: "ai:job:status:{jobId}" + fallback: "ai:fallback:{industry}:{region}" + ttl: + recommendation: 86400 + jobStatus: 86400 + fallback: 604800 + + kafka: + topics: + input: "ai-event-generation-job" + consumer: + groupId: "ai-service-consumers" + maxRetries: 3 + retryBackoffMs: 5000 + maxPollRecords: 10 + sessionTimeoutMs: 30000 + + external-apis: + claude: + endpoint: "https://api.anthropic.com/v1/messages" + model: "claude-3-5-sonnet-20241022" + maxTokens: 4096 + timeout: 300000 + gpt4: + endpoint: "https://api.openai.com/v1/chat/completions" + model: "gpt-4-turbo-preview" + maxTokens: 4096 + timeout: 300000 diff --git a/design/backend/api/analytics-service-api.yaml b/design/backend/api/analytics-service-api.yaml new file mode 100644 index 0000000..0303892 --- /dev/null +++ b/design/backend/api/analytics-service-api.yaml @@ -0,0 +1,1081 @@ +openapi: 3.0.3 +info: + title: Analytics Service API + description: | + ์‹ค์‹œ๊ฐ„ ํšจ๊ณผ ์ธก์ • ๋ฐ ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ์ œ๊ณตํ•˜๋Š” Analytics Service API + + **์ฃผ์š” ๊ธฐ๋Šฅ:** + - ์ด๋ฒคํŠธ ์„ฑ๊ณผ ๋Œ€์‹œ๋ณด๋“œ ์‹ค์‹œ๊ฐ„ ์กฐํšŒ + - ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ ๋ฐ ๋น„๊ต + - ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด ๋ถ„์„ + - ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ (ROI) ์ƒ์„ธ ๋ถ„์„ + + **Kafka Event Subscriptions:** + - EventCreated: ์ด๋ฒคํŠธ ํ†ต๊ณ„ ์ดˆ๊ธฐํ™” + - ParticipantRegistered: ์‹ค์‹œ๊ฐ„ ์ฐธ์—ฌ์ž ์ˆ˜ ์—…๋ฐ์ดํŠธ + - DistributionCompleted: ๋ฐฐํฌ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ + + **External API Integration:** + - ์šฐ๋ฆฌ๋™๋„คTV API (์กฐํšŒ์ˆ˜) + - ์ง€๋‹ˆTV API (๊ด‘๊ณ  ๋…ธ์ถœ ์ˆ˜) + - ๋ง๊ณ ๋น„์ฆˆ API (ํ†ตํ™” ์ˆ˜, ์™„๋ฃŒ ์ˆ˜, ํ‰๊ท  ํ†ตํ™” ์‹œ๊ฐ„) + - SNS APIs (์ข‹์•„์š”, ๋Œ“๊ธ€, ๊ณต์œ  ์ˆ˜) + - Circuit Breaker with fallback to cached data + + **Caching Strategy:** + - Redis cache with 5-minute TTL + - Cache-Aside pattern for dashboard data + - Real-time updates via Kafka event subscription + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8086 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/analytics/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/analytics/v1 + description: Production Server + +tags: + - name: Analytics + description: ์ด๋ฒคํŠธ ์„ฑ๊ณผ ๋ถ„์„ ๋ฐ ๋Œ€์‹œ๋ณด๋“œ API + - name: Channels + description: ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ API + - name: Timeline + description: ์‹œ๊ฐ„๋Œ€๋ณ„ ๋ถ„์„ API + - name: ROI + description: ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  ๋ถ„์„ API + +paths: + /events/{eventId}/analytics: + get: + tags: + - Analytics + summary: ์„ฑ๊ณผ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ + description: | + ์ด๋ฒคํŠธ์˜ ์ „์ฒด ์„ฑ๊ณผ๋ฅผ ํ†ตํ•ฉํ•˜์—ฌ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + - ์‹ค์‹œ๊ฐ„ ์ฐธ์—ฌ์ž ์ˆ˜ + - ์ด ๋„๋‹ฌ ์ˆ˜ (์กฐํšŒ์ˆ˜, ๋…ธ์ถœ ์ˆ˜) + - ์ฐธ์—ฌ์œจ, ์ „ํ™˜์œจ + - ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  (ROI) + - ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ์š”์•ฝ + operationId: getEventAnalytics + x-user-story: UFR-ANAL-010 + x-controller: AnalyticsDashboardController + parameters: + - name: eventId + in: path + required: true + description: ์ด๋ฒคํŠธ ID + schema: + type: string + example: "evt_2025012301" + - name: startDate + in: query + required: false + description: ์กฐํšŒ ์‹œ์ž‘ ๋‚ ์งœ (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + - name: endDate + in: query + required: false + description: ์กฐํšŒ ์ข…๋ฃŒ ๋‚ ์งœ (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-31T23:59:59Z" + - name: refresh + in: query + required: false + description: ์บ์‹œ ๊ฐฑ์‹  ์—ฌ๋ถ€ (true์ธ ๊ฒฝ์šฐ ์™ธ๋ถ€ API ํ˜ธ์ถœ) + schema: + type: boolean + default: false + responses: + '200': + description: ์„ฑ๊ณผ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/AnalyticsDashboard' + '404': + description: ์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: ์„œ๋ฒ„ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /events/{eventId}/analytics/channels: + get: + tags: + - Channels + summary: ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ + description: | + ๊ฐ ๋ฐฐํฌ ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ๋ฅผ ์ƒ์„ธํ•˜๊ฒŒ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + - ์šฐ๋ฆฌ๋™๋„คTV ์กฐํšŒ์ˆ˜ + - ์ง€๋‹ˆTV ๊ด‘๊ณ  ๋…ธ์ถœ ์ˆ˜ + - ๋ง๊ณ ๋น„์ฆˆ ํ†ตํ™” ์ˆ˜ ๋ฐ ์™„๋ฃŒ์œจ + - SNS ๋ฐ˜์‘ ์ˆ˜ (์ข‹์•„์š”, ๋Œ“๊ธ€, ๊ณต์œ ) + - ์ฑ„๋„๋ณ„ ์ฐธ์—ฌ์œจ ๋ฐ ์ „ํ™˜์œจ + - ์ฑ„๋„๋ณ„ ROI + operationId: getChannelAnalytics + x-user-story: UFR-ANAL-010 + x-controller: ChannelAnalyticsController + parameters: + - name: eventId + in: path + required: true + description: ์ด๋ฒคํŠธ ID + schema: + type: string + example: "evt_2025012301" + - name: channels + in: query + required: false + description: ์กฐํšŒํ•  ์ฑ„๋„ ๋ชฉ๋ก (์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„, ๋ฏธ์ง€์ • ์‹œ ์ „์ฒด) + schema: + type: string + example: "์šฐ๋ฆฌ๋™๋„คTV,์ง€๋‹ˆTV,SNS" + - name: sortBy + in: query + required: false + description: ์ •๋ ฌ ๊ธฐ์ค€ + schema: + type: string + enum: + - views + - participants + - engagement_rate + - conversion_rate + - roi + default: roi + - name: order + in: query + required: false + description: ์ •๋ ฌ ์ˆœ์„œ + schema: + type: string + enum: + - asc + - desc + default: desc + responses: + '200': + description: ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/ChannelAnalyticsResponse' + '404': + description: ์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: ์„œ๋ฒ„ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /events/{eventId}/analytics/timeline: + get: + tags: + - Timeline + summary: ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด + description: | + ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„ ๋™์•ˆ์˜ ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด๋ฅผ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + - ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ์ž ์ˆ˜ + - ์‹œ๊ฐ„๋Œ€๋ณ„ ์กฐํšŒ์ˆ˜ + - ํ”ผํฌ ํƒ€์ž„ ๋ถ„์„ + - ์ถ”์„ธ ๋ถ„์„ (์ฆ๊ฐ€/๊ฐ์†Œ) + operationId: getTimelineAnalytics + x-user-story: UFR-ANAL-010 + x-controller: TimelineAnalyticsController + parameters: + - name: eventId + in: path + required: true + description: ์ด๋ฒคํŠธ ID + schema: + type: string + example: "evt_2025012301" + - name: interval + in: query + required: false + description: ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ ๋‹จ์œ„ + schema: + type: string + enum: + - hourly + - daily + - weekly + default: daily + - name: startDate + in: query + required: false + description: ์กฐํšŒ ์‹œ์ž‘ ๋‚ ์งœ (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + - name: endDate + in: query + required: false + description: ์กฐํšŒ ์ข…๋ฃŒ ๋‚ ์งœ (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-31T23:59:59Z" + - name: metrics + in: query + required: false + description: ์กฐํšŒํ•  ์ง€ํ‘œ ๋ชฉ๋ก (์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„) + schema: + type: string + example: "participants,views,engagement" + responses: + '200': + description: ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/TimelineAnalyticsResponse' + '404': + description: ์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: ์„œ๋ฒ„ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /events/{eventId}/analytics/roi: + get: + tags: + - ROI + summary: ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  ์ƒ์„ธ + description: | + ์ด๋ฒคํŠธ์˜ ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ ์„ ์ƒ์„ธํ•˜๊ฒŒ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + - ์ด ํˆฌ์ž ๋น„์šฉ (์ œ์ž‘๋น„, ๋ฐฐํฌ๋น„, ์šด์˜๋น„) + - ์˜ˆ์ƒ ๋งค์ถœ ์ฆ๋Œ€ + - ROI ๊ณ„์‚ฐ + - ๋น„์šฉ ๋Œ€๋น„ ์ฐธ์—ฌ์ž ์ˆ˜ (CPA) + - ๋น„์šฉ ๋Œ€๋น„ ์ „ํ™˜ ์ˆ˜ (CPC) + operationId: getRoiAnalytics + x-user-story: UFR-ANAL-010 + x-controller: RoiAnalyticsController + parameters: + - name: eventId + in: path + required: true + description: ์ด๋ฒคํŠธ ID + schema: + type: string + example: "evt_2025012301" + - name: includeProjection + in: query + required: false + description: ์˜ˆ์ƒ ์ˆ˜์ต ํฌํ•จ ์—ฌ๋ถ€ + schema: + type: boolean + default: true + responses: + '200': + description: ROI ์ƒ์„ธ ๋ถ„์„ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/RoiAnalyticsResponse' + '404': + description: ์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: ์„œ๋ฒ„ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +components: + schemas: + AnalyticsDashboard: + type: object + description: ์ด๋ฒคํŠธ ์„ฑ๊ณผ ๋Œ€์‹œ๋ณด๋“œ + properties: + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt_2025012301" + eventTitle: + type: string + description: ์ด๋ฒคํŠธ ์ œ๋ชฉ + example: "์‹ ๋…„๋งž์ด 20% ํ• ์ธ ์ด๋ฒคํŠธ" + period: + $ref: '#/components/schemas/PeriodInfo' + summary: + $ref: '#/components/schemas/AnalyticsSummary' + channelPerformance: + type: array + description: ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ์š”์•ฝ + items: + $ref: '#/components/schemas/ChannelSummary' + roi: + $ref: '#/components/schemas/RoiSummary' + lastUpdatedAt: + type: string + format: date-time + description: ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ + example: "2025-01-23T10:30:00Z" + dataSource: + type: string + description: ๋ฐ์ดํ„ฐ ์ถœ์ฒ˜ + enum: + - real-time + - cached + - fallback + example: "cached" + required: + - eventId + - eventTitle + - period + - summary + - lastUpdatedAt + + PeriodInfo: + type: object + description: ์กฐํšŒ ๊ธฐ๊ฐ„ ์ •๋ณด + properties: + startDate: + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + endDate: + type: string + format: date-time + example: "2025-01-31T23:59:59Z" + durationDays: + type: integer + description: ๊ธฐ๊ฐ„ (์ผ) + example: 30 + required: + - startDate + - endDate + + AnalyticsSummary: + type: object + description: ์„ฑ๊ณผ ์š”์•ฝ + properties: + totalParticipants: + type: integer + description: ์ด ์ฐธ์—ฌ์ž ์ˆ˜ + example: 15420 + totalViews: + type: integer + description: ์ด ์กฐํšŒ์ˆ˜ + example: 125300 + totalReach: + type: integer + description: ์ด ๋„๋‹ฌ ์ˆ˜ + example: 98500 + engagementRate: + type: number + format: double + description: ์ฐธ์—ฌ์œจ (%) + example: 12.3 + conversionRate: + type: number + format: double + description: ์ „ํ™˜์œจ (%) + example: 3.8 + averageEngagementTime: + type: integer + description: ํ‰๊ท  ์ฐธ์—ฌ ์‹œ๊ฐ„ (์ดˆ) + example: 145 + socialInteractions: + $ref: '#/components/schemas/SocialInteractionStats' + required: + - totalParticipants + - totalViews + - totalReach + - engagementRate + - conversionRate + + SocialInteractionStats: + type: object + description: SNS ๋ฐ˜์‘ ํ†ต๊ณ„ + properties: + likes: + type: integer + description: ์ข‹์•„์š” ์ˆ˜ + example: 3450 + comments: + type: integer + description: ๋Œ“๊ธ€ ์ˆ˜ + example: 890 + shares: + type: integer + description: ๊ณต์œ  ์ˆ˜ + example: 1250 + required: + - likes + - comments + - shares + + VoiceCallStats: + type: object + description: ๋ง๊ณ ๋น„์ฆˆ ์Œ์„ฑ ํ†ตํ™” ํ†ต๊ณ„ + properties: + totalCalls: + type: integer + description: ์ด ํ†ตํ™” ์ˆ˜ + example: 3000 + completedCalls: + type: integer + description: ์™„๋ฃŒ๋œ ํ†ตํ™” ์ˆ˜ + example: 2500 + averageDuration: + type: integer + description: ํ‰๊ท  ํ†ตํ™” ์‹œ๊ฐ„ (์ดˆ) + example: 45 + completionRate: + type: number + format: double + description: ํ†ตํ™” ์™„๋ฃŒ์œจ (%) + example: 83.3 + required: + - totalCalls + - completedCalls + - averageDuration + + ChannelSummary: + type: object + description: ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ์š”์•ฝ + properties: + channelName: + type: string + description: ์ฑ„๋„๋ช… + example: "์šฐ๋ฆฌ๋™๋„คTV" + views: + type: integer + description: ์กฐํšŒ์ˆ˜ + example: 45000 + participants: + type: integer + description: ์ฐธ์—ฌ์ž ์ˆ˜ + example: 5500 + engagementRate: + type: number + format: double + description: ์ฐธ์—ฌ์œจ (%) + example: 12.2 + conversionRate: + type: number + format: double + description: ์ „ํ™˜์œจ (%) + example: 4.1 + roi: + type: number + format: double + description: ROI (%) + example: 280.5 + required: + - channelName + - views + - participants + + RoiSummary: + type: object + description: ROI ์š”์•ฝ + properties: + totalInvestment: + type: number + format: double + description: ์ด ํˆฌ์ž ๋น„์šฉ (์›) + example: 5000000 + expectedRevenue: + type: number + format: double + description: ์˜ˆ์ƒ ๋งค์ถœ ์ฆ๋Œ€ (์›) + example: 19025000 + netProfit: + type: number + format: double + description: ์ˆœ์ด์ต (์›) + example: 14025000 + roi: + type: number + format: double + description: ROI (%) + example: 280.5 + costPerAcquisition: + type: number + format: double + description: ๊ณ ๊ฐ ํš๋“ ๋น„์šฉ (CPA, ์›) + example: 324.35 + required: + - totalInvestment + - expectedRevenue + - roi + + ChannelAnalyticsResponse: + type: object + description: ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ ์‘๋‹ต + properties: + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt_2025012301" + channels: + type: array + description: ์ฑ„๋„๋ณ„ ์ƒ์„ธ ๋ถ„์„ + items: + $ref: '#/components/schemas/ChannelAnalytics' + comparison: + $ref: '#/components/schemas/ChannelComparison' + lastUpdatedAt: + type: string + format: date-time + description: ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ + example: "2025-01-23T10:30:00Z" + required: + - eventId + - channels + - lastUpdatedAt + + ChannelAnalytics: + type: object + description: ์ฑ„๋„๋ณ„ ์ƒ์„ธ ๋ถ„์„ + properties: + channelName: + type: string + description: ์ฑ„๋„๋ช… + example: "์šฐ๋ฆฌ๋™๋„คTV" + channelType: + type: string + description: ์ฑ„๋„ ์œ ํ˜• + enum: + - LOCAL_TV + - CABLE_TV + - VOICE_CALL + - SNS + - MOBILE_APP + example: "LOCAL_TV" + metrics: + $ref: '#/components/schemas/ChannelMetrics' + performance: + $ref: '#/components/schemas/ChannelPerformance' + costs: + $ref: '#/components/schemas/ChannelCosts' + externalApiStatus: + type: string + description: ์™ธ๋ถ€ API ์—ฐ๋™ ์ƒํƒœ + enum: + - success + - fallback + - failed + example: "success" + required: + - channelName + - channelType + - metrics + - performance + + ChannelMetrics: + type: object + description: ์ฑ„๋„ ์ง€ํ‘œ + properties: + impressions: + type: integer + description: ๋…ธ์ถœ ์ˆ˜ + example: 120000 + views: + type: integer + description: ์กฐํšŒ์ˆ˜ + example: 45000 + clicks: + type: integer + description: ํด๋ฆญ ์ˆ˜ + example: 8900 + participants: + type: integer + description: ์ฐธ์—ฌ์ž ์ˆ˜ + example: 5500 + conversions: + type: integer + description: ์ „ํ™˜ ์ˆ˜ + example: 1850 + socialInteractions: + $ref: '#/components/schemas/SocialInteractionStats' + voiceCallStats: + $ref: '#/components/schemas/VoiceCallStats' + required: + - views + - participants + + ChannelPerformance: + type: object + description: ์ฑ„๋„ ์„ฑ๊ณผ ์ง€ํ‘œ + properties: + clickThroughRate: + type: number + format: double + description: ํด๋ฆญ๋ฅ  (CTR, %) + example: 7.4 + engagementRate: + type: number + format: double + description: ์ฐธ์—ฌ์œจ (%) + example: 12.2 + conversionRate: + type: number + format: double + description: ์ „ํ™˜์œจ (%) + example: 4.1 + averageEngagementTime: + type: integer + description: ํ‰๊ท  ์ฐธ์—ฌ ์‹œ๊ฐ„ (์ดˆ) + example: 165 + bounceRate: + type: number + format: double + description: ์ดํƒˆ์œจ (%) + example: 35.8 + required: + - engagementRate + - conversionRate + + ChannelCosts: + type: object + description: ์ฑ„๋„๋ณ„ ๋น„์šฉ + properties: + distributionCost: + type: number + format: double + description: ๋ฐฐํฌ ๋น„์šฉ (์›) + example: 1500000 + costPerView: + type: number + format: double + description: ์กฐํšŒ๋‹น ๋น„์šฉ (CPV, ์›) + example: 33.33 + costPerClick: + type: number + format: double + description: ํด๋ฆญ๋‹น ๋น„์šฉ (CPC, ์›) + example: 168.54 + costPerAcquisition: + type: number + format: double + description: ๊ณ ๊ฐ ํš๋“ ๋น„์šฉ (CPA, ์›) + example: 272.73 + roi: + type: number + format: double + description: ROI (%) + example: 295.3 + required: + - distributionCost + - roi + + ChannelComparison: + type: object + description: ์ฑ„๋„ ๊ฐ„ ๋น„๊ต ๋ถ„์„ + properties: + bestPerforming: + type: object + description: ์ตœ๊ณ  ์„ฑ๊ณผ ์ฑ„๋„ + properties: + byViews: + type: string + example: "์šฐ๋ฆฌ๋™๋„คTV" + byEngagement: + type: string + example: "์ง€๋‹ˆTV" + byRoi: + type: string + example: "SNS" + averageMetrics: + type: object + description: ์ „์ฒด ์ฑ„๋„ ํ‰๊ท  ์ง€ํ‘œ + properties: + engagementRate: + type: number + format: double + example: 11.5 + conversionRate: + type: number + format: double + example: 3.9 + roi: + type: number + format: double + example: 275.8 + + TimelineAnalyticsResponse: + type: object + description: ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด ์‘๋‹ต + properties: + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt_2025012301" + interval: + type: string + description: ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ + enum: + - hourly + - daily + - weekly + example: "daily" + dataPoints: + type: array + description: ์‹œ๊ฐ„๋Œ€๋ณ„ ๋ฐ์ดํ„ฐ + items: + $ref: '#/components/schemas/TimelineDataPoint' + trends: + $ref: '#/components/schemas/TrendAnalysis' + peakTimes: + type: array + description: ํ”ผํฌ ํƒ€์ž„ ์ •๋ณด + items: + $ref: '#/components/schemas/PeakTimeInfo' + lastUpdatedAt: + type: string + format: date-time + description: ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ + example: "2025-01-23T10:30:00Z" + required: + - eventId + - interval + - dataPoints + - lastUpdatedAt + + TimelineDataPoint: + type: object + description: ์‹œ๊ฐ„๋Œ€๋ณ„ ๋ฐ์ดํ„ฐ ํฌ์ธํŠธ + properties: + timestamp: + type: string + format: date-time + description: ์‹œ๊ฐ„ + example: "2025-01-15T00:00:00Z" + participants: + type: integer + description: ์ฐธ์—ฌ์ž ์ˆ˜ + example: 450 + views: + type: integer + description: ์กฐํšŒ์ˆ˜ + example: 3500 + engagement: + type: integer + description: ์ฐธ์—ฌ ํ–‰๋™ ์ˆ˜ + example: 280 + conversions: + type: integer + description: ์ „ํ™˜ ์ˆ˜ + example: 45 + cumulativeParticipants: + type: integer + description: ๋ˆ„์  ์ฐธ์—ฌ์ž ์ˆ˜ + example: 5450 + required: + - timestamp + - participants + - views + + TrendAnalysis: + type: object + description: ์ถ”์„ธ ๋ถ„์„ + properties: + overallTrend: + type: string + description: ์ „์ฒด ์ถ”์„ธ + enum: + - increasing + - stable + - decreasing + example: "increasing" + growthRate: + type: number + format: double + description: ์ฆ๊ฐ€์œจ (%) + example: 15.3 + projectedParticipants: + type: integer + description: ์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜ (๊ธฐ๊ฐ„ ์ข…๋ฃŒ ์‹œ์ ) + example: 18500 + peakPeriod: + type: string + description: ํ”ผํฌ ๊ธฐ๊ฐ„ + example: "2025-01-15 ~ 2025-01-18" + required: + - overallTrend + + PeakTimeInfo: + type: object + description: ํ”ผํฌ ํƒ€์ž„ ์ •๋ณด + properties: + timestamp: + type: string + format: date-time + description: ํ”ผํฌ ์‹œ๊ฐ„ + example: "2025-01-15T14:00:00Z" + metric: + type: string + description: ํ”ผํฌ ์ง€ํ‘œ + enum: + - participants + - views + - engagement + - conversions + example: "participants" + value: + type: integer + description: ํ”ผํฌ ๊ฐ’ + example: 1250 + description: + type: string + description: ํ”ผํฌ ์„ค๋ช… + example: "์ฃผ๋ง ์˜คํ›„ ์ตœ๋Œ€ ์ฐธ์—ฌ" + + RoiAnalyticsResponse: + type: object + description: ROI ์ƒ์„ธ ๋ถ„์„ ์‘๋‹ต + properties: + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt_2025012301" + investment: + $ref: '#/components/schemas/InvestmentDetails' + revenue: + $ref: '#/components/schemas/RevenueDetails' + roi: + $ref: '#/components/schemas/RoiCalculation' + costEfficiency: + $ref: '#/components/schemas/CostEfficiency' + projection: + $ref: '#/components/schemas/RevenueProjection' + lastUpdatedAt: + type: string + format: date-time + description: ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ + example: "2025-01-23T10:30:00Z" + required: + - eventId + - investment + - revenue + - roi + - lastUpdatedAt + + InvestmentDetails: + type: object + description: ํˆฌ์ž ๋น„์šฉ ์ƒ์„ธ + properties: + contentCreation: + type: number + format: double + description: ์ฝ˜ํ…์ธ  ์ œ์ž‘๋น„ (์›) + example: 2000000 + distribution: + type: number + format: double + description: ๋ฐฐํฌ ๋น„์šฉ (์›) + example: 2500000 + operation: + type: number + format: double + description: ์šด์˜ ๋น„์šฉ (์›) + example: 500000 + total: + type: number + format: double + description: ์ด ํˆฌ์ž ๋น„์šฉ (์›) + example: 5000000 + breakdown: + type: array + description: ์ฑ„๋„๋ณ„ ๋น„์šฉ ์ƒ์„ธ + items: + type: object + properties: + channelName: + type: string + example: "์šฐ๋ฆฌ๋™๋„คTV" + cost: + type: number + format: double + example: 1500000 + required: + - total + + RevenueDetails: + type: object + description: ์ˆ˜์ต ์ƒ์„ธ + properties: + directSales: + type: number + format: double + description: ์ง์ ‘ ๋งค์ถœ (์›) + example: 12500000 + expectedSales: + type: number + format: double + description: ์˜ˆ์ƒ ์ถ”๊ฐ€ ๋งค์ถœ (์›) + example: 6525000 + brandValue: + type: number + format: double + description: ๋ธŒ๋žœ๋“œ ๊ฐ€์น˜ ํ–ฅ์ƒ ์ถ”์ •์•ก (์›) + example: 3000000 + total: + type: number + format: double + description: ์ด ์ˆ˜์ต (์›) + example: 19025000 + required: + - total + + RoiCalculation: + type: object + description: ROI ๊ณ„์‚ฐ + properties: + netProfit: + type: number + format: double + description: ์ˆœ์ด์ต (์›) + example: 14025000 + roiPercentage: + type: number + format: double + description: ROI (%) + example: 280.5 + breakEvenPoint: + type: string + format: date-time + description: ์†์ต๋ถ„๊ธฐ์  ๋„๋‹ฌ ์‹œ์  + example: "2025-01-10T15:30:00Z" + paybackPeriod: + type: integer + description: ํˆฌ์ž ํšŒ์ˆ˜ ๊ธฐ๊ฐ„ (์ผ) + example: 9 + required: + - netProfit + - roiPercentage + + CostEfficiency: + type: object + description: ๋น„์šฉ ํšจ์œจ์„ฑ + properties: + costPerParticipant: + type: number + format: double + description: ์ฐธ์—ฌ์ž๋‹น ๋น„์šฉ (์›) + example: 324.35 + costPerConversion: + type: number + format: double + description: ์ „ํ™˜๋‹น ๋น„์šฉ (์›) + example: 850.34 + costPerView: + type: number + format: double + description: ์กฐํšŒ๋‹น ๋น„์šฉ (์›) + example: 39.90 + revenuePerParticipant: + type: number + format: double + description: ์ฐธ์—ฌ์ž๋‹น ์ˆ˜์ต (์›) + example: 1234.25 + required: + - costPerParticipant + + RevenueProjection: + type: object + description: ์ˆ˜์ต ์˜ˆ์ธก + properties: + currentRevenue: + type: number + format: double + description: ํ˜„์žฌ ๋ˆ„์  ์ˆ˜์ต (์›) + example: 12500000 + projectedFinalRevenue: + type: number + format: double + description: ์˜ˆ์ƒ ์ตœ์ข… ์ˆ˜์ต (์›) + example: 21000000 + confidenceLevel: + type: number + format: double + description: ์˜ˆ์ธก ์‹ ๋ขฐ๋„ (%) + example: 85.5 + basedOn: + type: string + description: ์˜ˆ์ธก ๊ธฐ๋ฐ˜ + example: "ํ˜„์žฌ ์ถ”์„ธ ๋ฐ ๊ณผ๊ฑฐ ์œ ์‚ฌ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ" + + ErrorResponse: + type: object + description: ์˜ค๋ฅ˜ ์‘๋‹ต + properties: + timestamp: + type: string + format: date-time + description: ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ๊ฐ„ + example: "2025-01-23T10:30:00Z" + status: + type: integer + description: HTTP ์ƒํƒœ ์ฝ”๋“œ + example: 404 + error: + type: string + description: ์˜ค๋ฅ˜ ์œ ํ˜• + example: "Not Found" + message: + type: string + description: ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ + example: "์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." + path: + type: string + description: ์š”์ฒญ ๊ฒฝ๋กœ + example: "/api/events/evt_2025012301/analytics" + errorCode: + type: string + description: ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ์ฝ”๋“œ + example: "ANAL_001" + required: + - timestamp + - status + - error + - message + - path + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ + +security: + - bearerAuth: [] diff --git a/design/backend/api/content-service-api.yaml b/design/backend/api/content-service-api.yaml new file mode 100644 index 0000000..d8f9f45 --- /dev/null +++ b/design/backend/api/content-service-api.yaml @@ -0,0 +1,1158 @@ +openapi: 3.0.3 +info: + title: Content Service API + version: 1.0.0 + description: | + # KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - Content Service API + + ## ์ฃผ์š” ๊ธฐ๋Šฅ + - **SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ** (UFR-CONT-010): AI ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ด๋ฏธ์ง€ ์ž๋™ ์ƒ์„ฑ + - **์ฝ˜ํ…์ธ  ํŽธ์ง‘** (UFR-CONT-020): ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ์กฐํšŒ, ์žฌ์ƒ์„ฑ, ์‚ญ์ œ + - **3๊ฐ€์ง€ ์Šคํƒ€์ผ**: ์‹ฌํ”Œ(SIMPLE), ํ™”๋ คํ•œ(FANCY), ํŠธ๋ Œ๋””(TRENDY) + - **3๊ฐœ ํ”Œ๋žซํผ ์ตœ์ ํ™”**: Instagram (1080x1080), Naver (800x600), Kakao (800x800) + - **Redis ์บ์‹ฑ**: TTL 7์ผ, ๋™์ผ eventDraftId ์žฌ์š”์ฒญ ์‹œ ์บ์‹œ ๋ฐ˜ํ™˜ + - **CDN ์ด๋ฏธ์ง€ ์ €์žฅ**: Azure Blob Storage ๊ธฐ๋ฐ˜ + + ## ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์•„ํ‚คํ…์ฒ˜ + + ### Kafka Job Consumer + **Topic**: `image-generation-job` + + **์ฒ˜๋ฆฌ ํ๋ฆ„**: + 1. Kafka์—์„œ ์ด๋ฏธ์ง€ ์ƒ์„ฑ Job ์ˆ˜์‹  (EventService์—์„œ ๋ฐœํ–‰) + 2. Redis์—์„œ AI Service ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ ์กฐํšŒ + 3. Redis ์บ์‹œ์—์„œ ๊ธฐ์กด ์ด๋ฏธ์ง€ ํ™•์ธ (๋™์ผ eventDraftId) + 4. ์™ธ๋ถ€ ์ด๋ฏธ์ง€ ์ƒ์„ฑ API ํ˜ธ์ถœ (Stable Diffusion / DALL-E) + - **Circuit Breaker**: 5๋ถ„ ํƒ€์ž„์•„์›ƒ, ์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open + - **Fallback**: Stable Diffusion โ†’ DALL-E โ†’ ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์ด๋ฏธ์ง€ + 5. ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ CDN(Azure Blob) ์—…๋กœ๋“œ + 6. Redis์— ์ด๋ฏธ์ง€ URL ์ €์žฅ (TTL 7์ผ) + 7. Job ์ƒํƒœ ์—…๋ฐ์ดํŠธ (PENDING โ†’ PROCESSING โ†’ COMPLETED/FAILED) + + **Job Payload Schema**: `ImageGenerationJob` (components/schemas ์ฐธ์กฐ) + + ## ์™ธ๋ถ€ API ์—ฐ๋™ + - **Image Generation API**: Stable Diffusion / DALL-E + - **Circuit Breaker**: 5๋ถ„ ํƒ€์ž„์•„์›ƒ, 50% ์‹คํŒจ์œจ ์ž„๊ณ„๊ฐ’ + - **CDN**: Azure Blob Storage (์ด๋ฏธ์ง€ ์—…๋กœ๋“œ) + + ## ์ถœ๋ ฅ ํ˜•์‹ + - ์Šคํƒ€์ผ๋ณ„ 3๊ฐœ ร— ํ”Œ๋žซํผ๋ณ„ 3๊ฐœ = **์ด 9๊ฐœ ์ด๋ฏธ์ง€** ์ƒ์„ฑ (ํ˜„์žฌ๋Š” Instagram๋งŒ) + - CDN URL ๋ฐ˜ํ™˜ + + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8082 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/content/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/content/v1 + description: Production Server + +tags: + - name: Job Status + description: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ž‘์—… ์ƒํƒœ ์กฐํšŒ (๋น„๋™๊ธฐ ํด๋ง) + - name: Content Management + description: ์ƒ์„ฑ๋œ ์ฝ˜ํ…์ธ  ์กฐํšŒ ๋ฐ ๊ด€๋ฆฌ (UFR-CONT-020) + - name: Image Management + description: ์ด๋ฏธ์ง€ ์žฌ์ƒ์„ฑ ๋ฐ ์‚ญ์ œ (UFR-CONT-020) + +paths: + /content/images/generate: + post: + tags: + - Job Status + summary: SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ (๋น„๋™๊ธฐ) + description: | + ์ด๋ฒคํŠธ ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ 3๊ฐ€์ง€ ์Šคํƒ€์ผ์˜ SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ์„ ๋น„๋™๊ธฐ๋กœ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. + + ## ์ฒ˜๋ฆฌ ๋ฐฉ์‹ + - **๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ**: Kafka `image-generation-job` ํ† ํ”ฝ์— Job ๋ฐœํ–‰ + - **ํด๋ง ์กฐํšŒ**: jobId๋กœ ์ƒ์„ฑ ์ƒํƒœ ์กฐํšŒ (GET /content/images/jobs/{jobId}) + - **์บ์‹ฑ**: ๋™์ผํ•œ eventDraftId ์žฌ์š”์ฒญ ์‹œ ์บ์‹œ ๋ฐ˜ํ™˜ (TTL 7์ผ) + + ## ์ƒ์„ฑ ์Šคํƒ€์ผ + 1. **์‹ฌํ”Œ ์Šคํƒ€์ผ (SIMPLE)**: ๊น”๋”ํ•œ ๋””์ž์ธ, ํ…์ŠคํŠธ ์ค‘์‹ฌ + 2. **ํ™”๋ คํ•œ ์Šคํƒ€์ผ (FANCY)**: ๋ˆˆ์— ๋„๋Š” ๋””์ž์ธ, ํ’๋ถ€ํ•œ ์ƒ‰์ƒ + 3. **ํŠธ๋ Œ๋”” ์Šคํƒ€์ผ (TRENDY)**: ์ตœ์‹  ํŠธ๋ Œ๋“œ, MZ์„ธ๋Œ€ ํƒ€๊ฒŸ + + ## Resilience ํŒจํ„ด + - Circuit Breaker (5๋ถ„ ํƒ€์ž„์•„์›ƒ, ์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open) + - Fallback (Stable Diffusion โ†’ DALL-E โ†’ ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ) + + operationId: generateImages + x-user-story: UFR-CONT-010 + x-controller: ImageGenerationController.generateImages + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ImageGenerationRequest" + examples: + basicEvent: + summary: ๊ธฐ๋ณธ ์ด๋ฒคํŠธ + value: + eventDraftId: "evt-draft-12345" + eventInfo: + title: "๋ด„๋งž์ด ์ปคํ”ผ ํ• ์ธ ์ด๋ฒคํŠธ" + giftName: "์•„๋ฉ”๋ฆฌ์นด๋…ธ 1+1" + brandColor: "#FF5733" + logoUrl: "https://cdn.example.com/logo.png" + withoutLogo: + summary: ๋กœ๊ณ  ์—†๋Š” ์ด๋ฒคํŠธ + value: + eventDraftId: "evt-draft-67890" + eventInfo: + title: "์‹ ๋ฉ”๋‰ด ์ถœ์‹œ ๊ธฐ๋… ๊ฒฝํ’ˆ ์ถ”์ฒจ" + giftName: "์Šคํƒ€๋ฒ…์Šค ๊ธฐํ”„ํ‹ฐ์ฝ˜ 5000์›๊ถŒ" + brandColor: "#00704A" + + responses: + "202": + description: | + ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + jobId๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ์„ฑ ์ƒํƒœ๋ฅผ ํด๋ง ์กฐํšŒํ•˜์„ธ์š”. + content: + application/json: + schema: + $ref: "#/components/schemas/ImageGenerationAcceptedResponse" + examples: + accepted: + summary: ์š”์ฒญ ์ ‘์ˆ˜ ์„ฑ๊ณต + value: + jobId: "job-img-abc123" + eventDraftId: "evt-draft-12345" + status: "PENDING" + estimatedCompletionTime: 5 + message: "์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ์ด ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. jobId๋กœ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•˜์„ธ์š”." + + "400": + description: ์ž˜๋ชป๋œ ์š”์ฒญ (ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ, ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ) + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + missingField: + summary: ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ + value: + code: "BAD_REQUEST" + message: "eventInfo.title์€ ํ•„์ˆ˜ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค." + timestamp: "2025-10-22T14:30:00Z" + invalidColor: + summary: ์œ ํšจํ•˜์ง€ ์•Š์€ ์ƒ‰์ƒ ์ฝ”๋“œ + value: + code: "BAD_REQUEST" + message: "brandColor๋Š” HEX ์ƒ‰์ƒ ์ฝ”๋“œ ํ˜•์‹์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." + timestamp: "2025-10-22T14:30:00Z" + + "429": + description: ์š”์ฒญ ์ œํ•œ ์ดˆ๊ณผ (Rate Limiting) + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + rateLimitExceeded: + summary: ์š”์ฒญ ์ œํ•œ ์ดˆ๊ณผ + value: + code: "RATE_LIMIT_EXCEEDED" + message: "์š”์ฒญ ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค. 1๋ถ„ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”." + timestamp: "2025-10-22T14:30:00Z" + retryAfter: 60 + + "500": + description: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + internalError: + summary: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ + value: + code: "INTERNAL_SERVER_ERROR" + message: "์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + timestamp: "2025-10-22T14:30:00Z" + + security: + - BearerAuth: [] + + /content/images/jobs/{jobId}: + get: + tags: + - Job Status + summary: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ž‘์—… ์ƒํƒœ ์กฐํšŒ (ํด๋ง) + description: | + jobId๋กœ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + + ## ํด๋ง ๊ถŒ์žฅ์‚ฌํ•ญ + - **ํด๋ง ๊ฐ„๊ฒฉ**: 2์ดˆ + - **์ตœ๋Œ€ ํด๋ง ์‹œ๊ฐ„**: 30์ดˆ + - **Timeout ํ›„ ์ฒ˜๋ฆฌ**: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ๋ฐ ์žฌ์‹œ๋„ ์˜ต์…˜ ์ œ๊ณต + + ## ์ƒํƒœ ์ข…๋ฅ˜ + - **PENDING**: ๋Œ€๊ธฐ ์ค‘ (Kafka Queue์—์„œ ๋Œ€๊ธฐ) + - **PROCESSING**: ์ƒ์„ฑ ์ค‘ (AI API ํ˜ธ์ถœ ์ง„ํ–‰) + - **COMPLETED**: ์™„๋ฃŒ (3๊ฐ€์ง€ ์ด๋ฏธ์ง€ URL ๋ฐ˜ํ™˜) + - **FAILED**: ์‹คํŒจ (์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํฌํ•จ, Fallback ์ด๋ฏธ์ง€ ์ œ๊ณต) + + ## ์บ์‹ฑ + - COMPLETED ์ƒํƒœ๋Š” Redis ์บ์‹ฑ (TTL 7์ผ) + - ๋™์ผํ•œ eventDraftId ์žฌ์š”์ฒญ ์‹œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + + operationId: getImageGenerationStatus + x-user-story: UFR-CONT-010 + x-controller: ImageGenerationController.getJobStatus + parameters: + - name: jobId + in: path + required: true + description: ์ด๋ฏธ์ง€ ์ƒ์„ฑ Job ID + schema: + type: string + example: "job-img-abc123" + + responses: + "200": + description: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ƒํƒœ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: "#/components/schemas/ImageGenerationStatusResponse" + examples: + pending: + summary: ๋Œ€๊ธฐ ์ค‘ + value: + jobId: "job-img-abc123" + status: "PENDING" + message: "์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋Œ€๊ธฐ ์ค‘์ž…๋‹ˆ๋‹ค." + estimatedCompletionTime: 5 + + processing: + summary: ์ƒ์„ฑ ์ค‘ + value: + jobId: "job-img-abc123" + status: "PROCESSING" + message: "AI๊ฐ€ ์ด๋ฒคํŠธ์— ์–ด์šธ๋ฆฌ๋Š” ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์žˆ์–ด์š”..." + estimatedCompletionTime: 3 + + completed: + summary: ์ƒ์„ฑ ์™„๋ฃŒ + value: + jobId: "job-img-abc123" + status: "COMPLETED" + message: "์ด๋ฏธ์ง€ ์ƒ์„ฑ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + images: + - style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "FANCY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-fancy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "TRENDY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-trendy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + completedAt: "2025-10-22T14:30:05Z" + fromCache: false + + completedFromCache: + summary: ์บ์‹œ์—์„œ ๋ฐ˜ํ™˜ + value: + jobId: "job-img-def456" + status: "COMPLETED" + message: "์ด๋ฏธ์ง€ ์ƒ์„ฑ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (์บ์‹œ)" + images: + - style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "FANCY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-fancy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "TRENDY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-trendy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + completedAt: "2025-10-22T14:28:00Z" + fromCache: true + + failed: + summary: ์ƒ์„ฑ ์‹คํŒจ + value: + jobId: "job-img-abc123" + status: "FAILED" + message: "์ด๋ฏธ์ง€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค." + error: + code: "IMAGE_GENERATION_FAILED" + detail: "์™ธ๋ถ€ AI API ์‘๋‹ต ์‹œ๊ฐ„ ์ดˆ๊ณผ. ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ๋Œ€์ฒด๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + completedAt: "2025-10-22T14:30:25Z" + + "404": + description: Job ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + notFound: + summary: Job ID ์—†์Œ + value: + code: "NOT_FOUND" + message: "Job ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." + timestamp: "2025-10-22T14:30:00Z" + + "500": + description: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + internalError: + summary: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ + value: + code: "INTERNAL_SERVER_ERROR" + message: "์ƒํƒœ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + timestamp: "2025-10-22T14:30:00Z" + + security: + - BearerAuth: [] + + /content/events/{eventDraftId}: + get: + tags: + - Content Management + summary: ์ด๋ฒคํŠธ์˜ ์ƒ์„ฑ๋œ ์ฝ˜ํ…์ธ  ์กฐํšŒ + description: | + ํŠน์ • ์ด๋ฒคํŠธ์˜ ์ƒ์„ฑ๋œ ๋ชจ๋“  ์ฝ˜ํ…์ธ (์ด๋ฏธ์ง€) ์กฐํšŒ + - Redis ์บ์‹œ์—์„œ ์กฐํšŒ + - TTL 7์ผ ์ด๋‚ด ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ ๊ฐ€๋Šฅ + - ์บ์‹œ ๋งŒ๋ฃŒ ์‹œ 404 ๋ฐ˜ํ™˜ + + operationId: getContentByEventId + x-user-story: UFR-CONT-020 + x-controller: ContentController.getContentByEventId + parameters: + - name: eventDraftId + in: path + required: true + description: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ID + schema: + type: string + example: "evt-draft-12345" + + responses: + "200": + description: ์ฝ˜ํ…์ธ  ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: "#/components/schemas/ContentResponse" + examples: + success: + summary: ์ฝ˜ํ…์ธ  ์กฐํšŒ ์„ฑ๊ณต + value: + eventDraftId: "evt-draft-12345" + images: + - imageId: "img-12345-simple" + style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:05Z" + - imageId: "img-12345-fancy" + style: "FANCY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-fancy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:10Z" + - imageId: "img-12345-trendy" + style: "TRENDY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-trendy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:15Z" + totalCount: 3 + createdAt: "2025-10-22T14:30:00Z" + expiresAt: "2025-10-29T14:30:00Z" + + "404": + description: ์ฝ˜ํ…์ธ ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ (์ƒ์„ฑ ์ค‘์ด๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋จ) + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + examples: + notFound: + summary: ์ฝ˜ํ…์ธ  ์—†์Œ + value: + code: "CONTENT_NOT_FOUND" + message: "ํ•ด๋‹น ์ด๋ฒคํŠธ์˜ ์ฝ˜ํ…์ธ ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." + timestamp: "2025-10-22T14:30:00Z" + + "500": + description: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + security: + - BearerAuth: [] + + /content/events/{eventDraftId}/images: + get: + tags: + - Content Management + summary: ์ด๋ฒคํŠธ์˜ ์ด๋ฏธ์ง€ ๋ชฉ๋ก ์กฐํšŒ (ํ•„ํ„ฐ๋ง) + description: | + ํŠน์ • ์ด๋ฒคํŠธ์˜ ๋ชจ๋“  ์ƒ์„ฑ ์ด๋ฏธ์ง€ ๋ชฉ๋ก ์กฐํšŒ + - ์Šคํƒ€์ผ๋ณ„, ํ”Œ๋žซํผ๋ณ„ ํ•„ํ„ฐ๋ง ์ง€์› + + operationId: getImages + x-user-story: UFR-CONT-020 + x-controller: ContentController.getImages + parameters: + - name: eventDraftId + in: path + required: true + description: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ID + schema: + type: string + example: "evt-draft-12345" + - name: style + in: query + required: false + description: ์ด๋ฏธ์ง€ ์Šคํƒ€์ผ ํ•„ํ„ฐ + schema: + type: string + enum: [SIMPLE, FANCY, TRENDY] + example: "SIMPLE" + - name: platform + in: query + required: false + description: ํ”Œ๋žซํผ ํ•„ํ„ฐ + schema: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + example: "INSTAGRAM" + + responses: + "200": + description: ์ด๋ฏธ์ง€ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + type: object + properties: + eventDraftId: + type: string + totalCount: + type: integer + images: + type: array + items: + $ref: "#/components/schemas/GeneratedImage" + examples: + allImages: + summary: ์ „์ฒด ์ด๋ฏธ์ง€ ์กฐํšŒ + value: + eventDraftId: "evt-draft-12345" + totalCount: 3 + images: + - imageId: "img-12345-simple" + style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:05Z" + + "404": + description: ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + security: + - BearerAuth: [] + + /content/images/{imageId}: + get: + tags: + - Image Management + summary: ํŠน์ • ์ด๋ฏธ์ง€ ์ƒ์„ธ ์กฐํšŒ + description: ์ด๋ฏธ์ง€ ID๋กœ ํŠน์ • ์ด๋ฏธ์ง€์˜ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ + + operationId: getImageById + x-user-story: UFR-CONT-020 + x-controller: ContentController.getImageById + parameters: + - name: imageId + in: path + required: true + description: ์ด๋ฏธ์ง€ ID + schema: + type: string + example: "img-12345-simple" + + responses: + "200": + description: ์ด๋ฏธ์ง€ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: "#/components/schemas/GeneratedImage" + examples: + success: + summary: ์ด๋ฏธ์ง€ ์กฐํšŒ ์„ฑ๊ณต + value: + imageId: "img-12345-simple" + style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:05Z" + + "404": + description: ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + security: + - BearerAuth: [] + + delete: + tags: + - Image Management + summary: ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ์‚ญ์ œ + description: | + ํŠน์ • ์ด๋ฏธ์ง€ ์‚ญ์ œ + - Redis ์บ์‹œ์—์„œ ์ œ๊ฑฐ + - CDN ์ด๋ฏธ์ง€๋Š” ์œ ์ง€ (๋น„์šฉ ๊ณ ๋ ค) + + operationId: deleteImage + x-user-story: UFR-CONT-020 + x-controller: ContentController.deleteImage + parameters: + - name: imageId + in: path + required: true + description: ์ด๋ฏธ์ง€ ID + schema: + type: string + example: "img-12345-simple" + + responses: + "204": + description: ์ด๋ฏธ์ง€ ์‚ญ์ œ ์„ฑ๊ณต + + "404": + description: ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + security: + - BearerAuth: [] + + /content/images/{imageId}/regenerate: + post: + tags: + - Image Management + summary: ์ด๋ฏธ์ง€ ์žฌ์ƒ์„ฑ ์š”์ฒญ + description: | + ํŠน์ • ์ด๋ฏธ์ง€๋ฅผ ์žฌ์ƒ์„ฑ (์ฝ˜ํ…์ธ  ํŽธ์ง‘) + - ๋™์ผํ•œ ์Šคํƒ€์ผ/ํ”Œ๋žซํผ์œผ๋กœ ์žฌ์ƒ์„ฑ + - ํ”„๋กฌํ”„ํŠธ ์ˆ˜์ • ๊ฐ€๋Šฅ + - ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ (Kafka Job ๋ฐœํ–‰) + + operationId: regenerateImage + x-user-story: UFR-CONT-020 + x-controller: ContentController.regenerateImage + parameters: + - name: imageId + in: path + required: true + description: ์ด๋ฏธ์ง€ ID + schema: + type: string + example: "img-12345-simple" + + requestBody: + required: false + description: ์žฌ์ƒ์„ฑ ์˜ต์…˜ (์„ ํƒ์‚ฌํ•ญ) + content: + application/json: + schema: + $ref: "#/components/schemas/ImageRegenerationRequest" + examples: + modifyPrompt: + summary: ํ”„๋กฌํ”„ํŠธ ์ˆ˜์ • + value: + content: "๋ฐ์€ ๋ถ„์œ„๊ธฐ๋กœ ๋ณ€๊ฒฝ" + changeStyle: + summary: ์Šคํƒ€์ผ ๋ณ€๊ฒฝ + value: + style: "FANCY" + + responses: + "202": + description: ์žฌ์ƒ์„ฑ ์š”์ฒญ ์ ‘์ˆ˜ (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) + content: + application/json: + schema: + type: object + required: + - message + - jobId + - estimatedTime + properties: + message: + type: string + example: "์ด๋ฏธ์ง€ ์žฌ์ƒ์„ฑ ์š”์ฒญ์ด ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + jobId: + type: string + example: "job-regen-abc123" + estimatedTime: + type: integer + description: ์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„ (์ดˆ) + example: 10 + + "404": + description: ์›๋ณธ ์ด๋ฏธ์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + + "503": + description: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์„œ๋น„์Šค ์žฅ์•  (Circuit Breaker Open) + content: + application/json: + schema: + $ref: "#/components/schemas/CircuitBreakerErrorResponse" + + security: + - BearerAuth: [] + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT ํ† ํฐ์„ Authorization ํ—ค๋”์— ํฌํ•จ (Bearer {token}) + + schemas: + # ======================================== + # Kafka Job Schema (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) + # ======================================== + ImageGenerationJob: + type: object + description: | + **Kafka Topic**: `image-generation-job` + + Event Service์—์„œ ๋ฐœํ–‰ํ•˜์—ฌ Content Service๊ฐ€ ์†Œ๋น„ํ•˜๋Š” Job Payload + - Content Service์˜ Kafka Consumer๊ฐ€ ์ฒ˜๋ฆฌ + - ๋น„๋™๊ธฐ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ž‘์—… ์ˆ˜ํ–‰ + required: + - jobId + - eventDraftId + - eventInfo + properties: + jobId: + type: string + description: Job ID (์ž‘์—… ์ถ”์ ์šฉ) + example: "job-img-abc123" + + eventDraftId: + type: string + description: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ID + example: "evt-draft-12345" + + eventInfo: + type: object + description: ์ด๋ฒคํŠธ ์ •๋ณด (AI Service์—์„œ ์ƒ์„ฑ) + required: + - title + - giftName + properties: + title: + type: string + description: ์ด๋ฒคํŠธ ์ œ๋ชฉ + example: "๋ด„๋งž์ด ์ปคํ”ผ ํ• ์ธ ์ด๋ฒคํŠธ" + giftName: + type: string + description: ๊ฒฝํ’ˆ๋ช… + example: "์•„๋ฉ”๋ฆฌ์นด๋…ธ 1+1" + brandColor: + type: string + description: ๋ธŒ๋žœ๋“œ ์ปฌ๋Ÿฌ (HEX) + pattern: "^#[0-9A-Fa-f]{6}$" + example: "#FF5733" + logoUrl: + type: string + format: uri + description: ๋กœ๊ณ  ์ด๋ฏธ์ง€ URL (์„ ํƒ) + example: "https://cdn.example.com/logo.png" + + styles: + type: array + description: ์ƒ์„ฑํ•  ์Šคํƒ€์ผ ๋ชฉ๋ก (๊ธฐ๋ณธ๊ฐ’ ์ „์ฒด) + items: + type: string + enum: [SIMPLE, FANCY, TRENDY] + example: ["SIMPLE", "FANCY", "TRENDY"] + + platforms: + type: array + description: ์ƒ์„ฑํ•  ํ”Œ๋žซํผ ๋ชฉ๋ก (๊ธฐ๋ณธ๊ฐ’ Instagram) + items: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + example: ["INSTAGRAM"] + + priority: + type: integer + description: ์šฐ์„ ์ˆœ์œ„ (1-10, ๋†’์„์ˆ˜๋ก ์šฐ์„ ) + minimum: 1 + maximum: 10 + example: 5 + + requestedAt: + type: string + format: date-time + description: ์š”์ฒญ ์‹œ๊ฐ + example: "2025-10-22T14:00:00Z" + + # ======================================== + # Request Schemas + # ======================================== + ImageGenerationRequest: + type: object + required: + - eventDraftId + - eventInfo + properties: + eventDraftId: + type: string + description: | + ์ด๋ฒคํŠธ ์ดˆ์•ˆ ID (Event Service์—์„œ ๋ฐœ๊ธ‰) + ๋™์ผํ•œ eventDraftId๋กœ ์žฌ์š”์ฒญ ์‹œ ์บ์‹œ๋œ ์ด๋ฏธ์ง€ ๋ฐ˜ํ™˜ + example: "evt-draft-12345" + + eventInfo: + type: object + required: + - title + - giftName + properties: + title: + type: string + description: ์ด๋ฒคํŠธ ์ œ๋ชฉ (์ตœ๋Œ€ 50์ž) + minLength: 1 + maxLength: 50 + example: "๋ด„๋งž์ด ์ปคํ”ผ ํ• ์ธ ์ด๋ฒคํŠธ" + + giftName: + type: string + description: ๊ฒฝํ’ˆ๋ช… (์ตœ๋Œ€ 30์ž) + minLength: 1 + maxLength: 30 + example: "์•„๋ฉ”๋ฆฌ์นด๋…ธ 1+1" + + brandColor: + type: string + description: | + ๋ธŒ๋žœ๋“œ ์ปฌ๋Ÿฌ (HEX ์ƒ‰์ƒ ์ฝ”๋“œ) + ์‚ฌ์šฉ์ž ํ”„๋กœํ•„์—์„œ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜ ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ + pattern: "^#[0-9A-Fa-f]{6}$" + example: "#FF5733" + + logoUrl: + type: string + format: uri + description: | + ๋กœ๊ณ  ์ด๋ฏธ์ง€ URL (์„ ํƒ) + ์—…๋กœ๋“œ๋œ ๊ฒฝ์šฐ์—๋งŒ ์ œ๊ณต + example: "https://cdn.example.com/logo.png" + + ImageGenerationAcceptedResponse: + type: object + required: + - jobId + - eventDraftId + - status + - message + properties: + jobId: + type: string + description: ์ด๋ฏธ์ง€ ์ƒ์„ฑ Job ID (ํด๋ง ์กฐํšŒ์— ์‚ฌ์šฉ) + example: "job-img-abc123" + + eventDraftId: + type: string + description: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ID + example: "evt-draft-12345" + + status: + type: string + enum: [PENDING] + description: ์ดˆ๊ธฐ ์ƒํƒœ๋Š” ํ•ญ์ƒ PENDING + example: "PENDING" + + estimatedCompletionTime: + type: integer + description: ์˜ˆ์ƒ ์™„๋ฃŒ ์‹œ๊ฐ„ (์ดˆ) + example: 5 + + message: + type: string + description: ์‘๋‹ต ๋ฉ”์‹œ์ง€ + example: "์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ์ด ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. jobId๋กœ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•˜์„ธ์š”." + + ImageGenerationStatusResponse: + type: object + required: + - jobId + - status + - message + properties: + jobId: + type: string + description: ์ด๋ฏธ์ง€ ์ƒ์„ฑ Job ID + example: "job-img-abc123" + + status: + type: string + enum: [PENDING, PROCESSING, COMPLETED, FAILED] + description: | + Job ์ƒํƒœ + - PENDING: ๋Œ€๊ธฐ ์ค‘ + - PROCESSING: ์ƒ์„ฑ ์ค‘ + - COMPLETED: ์™„๋ฃŒ + - FAILED: ์‹คํŒจ + example: "COMPLETED" + + message: + type: string + description: ์ƒํƒœ ๋ฉ”์‹œ์ง€ + example: "์ด๋ฏธ์ง€ ์ƒ์„ฑ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + + estimatedCompletionTime: + type: integer + description: ์˜ˆ์ƒ ์™„๋ฃŒ ์‹œ๊ฐ„ (์ดˆ, PENDING/PROCESSING ์ƒํƒœ์—์„œ๋งŒ) + example: 3 + + images: + type: array + description: ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ๋ฐฐ์—ด (COMPLETED ์ƒํƒœ์—์„œ๋งŒ) + items: + $ref: "#/components/schemas/GeneratedImage" + + completedAt: + type: string + format: date-time + description: ์™„๋ฃŒ ์‹œ๊ฐ (COMPLETED/FAILED ์ƒํƒœ์—์„œ๋งŒ) + example: "2025-10-22T14:30:05Z" + + fromCache: + type: boolean + description: ์บ์‹œ์—์„œ ๋ฐ˜ํ™˜ ์—ฌ๋ถ€ (COMPLETED ์ƒํƒœ์—์„œ๋งŒ) + example: false + + error: + $ref: "#/components/schemas/JobError" + + GeneratedImage: + type: object + required: + - style + - url + - platform + - size + properties: + style: + type: string + enum: [SIMPLE, FANCY, TRENDY] + description: | + ์ด๋ฏธ์ง€ ์Šคํƒ€์ผ + - SIMPLE: ์‹ฌํ”Œ ์Šคํƒ€์ผ (๊น”๋”ํ•œ ๋””์ž์ธ, ํ…์ŠคํŠธ ์ค‘์‹ฌ) + - FANCY: ํ™”๋ คํ•œ ์Šคํƒ€์ผ (๋ˆˆ์— ๋„๋Š” ๋””์ž์ธ, ํ’๋ถ€ํ•œ ์ƒ‰์ƒ) + - TRENDY: ํŠธ๋ Œ๋”” ์Šคํƒ€์ผ (์ตœ์‹  ํŠธ๋ Œ๋“œ, MZ์„ธ๋Œ€ ํƒ€๊ฒŸ) + example: "SIMPLE" + + url: + type: string + format: uri + description: CDN ์ด๋ฏธ์ง€ URL + example: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + + platform: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + description: | + ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™” + - INSTAGRAM: 1080x1080 + - NAVER_BLOG: 800x600 + - KAKAO_CHANNEL: 800x800 + example: "INSTAGRAM" + + size: + type: object + required: + - width + - height + properties: + width: + type: integer + description: ์ด๋ฏธ์ง€ ๋„ˆ๋น„ (ํ”ฝ์…€) + example: 1080 + + height: + type: integer + description: ์ด๋ฏธ์ง€ ๋†’์ด (ํ”ฝ์…€) + example: 1080 + + JobError: + type: object + required: + - code + - detail + description: Job ์‹คํŒจ ์‹œ ์—๋Ÿฌ ์ •๋ณด (FAILED ์ƒํƒœ์—์„œ๋งŒ) + properties: + code: + type: string + description: ์—๋Ÿฌ ์ฝ”๋“œ + example: "IMAGE_GENERATION_FAILED" + + detail: + type: string + description: ์ƒ์„ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + example: "์™ธ๋ถ€ AI API ์‘๋‹ต ์‹œ๊ฐ„ ์ดˆ๊ณผ. ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ๋Œ€์ฒด๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + + ErrorResponse: + type: object + required: + - code + - message + - timestamp + properties: + code: + type: string + description: ์—๋Ÿฌ ์ฝ”๋“œ + example: "BAD_REQUEST" + + message: + type: string + description: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + example: "eventInfo.title์€ ํ•„์ˆ˜ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค." + + timestamp: + type: string + format: date-time + description: ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ๊ฐ + example: "2025-10-22T14:30:00Z" + + retryAfter: + type: integer + description: ์žฌ์‹œ๋„ ๋Œ€๊ธฐ ์‹œ๊ฐ„ (์ดˆ, Rate Limiting ์—๋Ÿฌ์—์„œ๋งŒ) + example: 60 + + # ======================================== + # Content Management Schemas (UFR-CONT-020) + # ======================================== + ContentResponse: + type: object + description: ์ด๋ฒคํŠธ์˜ ์ƒ์„ฑ๋œ ์ฝ˜ํ…์ธ  ์ „์ฒด ์ •๋ณด + required: + - eventDraftId + - images + - totalCount + - createdAt + - expiresAt + properties: + eventDraftId: + type: string + description: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ID + example: "evt-draft-12345" + + images: + type: array + description: ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ๋ชฉ๋ก + items: + $ref: "#/components/schemas/ImageDetail" + + totalCount: + type: integer + description: ์ด ์ด๋ฏธ์ง€ ๊ฐœ์ˆ˜ + example: 3 + + createdAt: + type: string + format: date-time + description: ์ฝ˜ํ…์ธ  ์ƒ์„ฑ ์‹œ๊ฐ + example: "2025-10-22T14:30:00Z" + + expiresAt: + type: string + format: date-time + description: ์บ์‹œ ๋งŒ๋ฃŒ ์‹œ๊ฐ (TTL 7์ผ) + example: "2025-10-29T14:30:00Z" + + ImageDetail: + type: object + description: ์ƒ์„ธ ์ด๋ฏธ์ง€ ์ •๋ณด (์ƒ์„ฑ ์‹œ๊ฐ ํฌํ•จ) + required: + - imageId + - style + - url + - platform + - size + - createdAt + properties: + imageId: + type: string + description: ์ด๋ฏธ์ง€ ID + example: "img-12345-simple" + + style: + type: string + enum: [SIMPLE, FANCY, TRENDY] + description: ์ด๋ฏธ์ง€ ์Šคํƒ€์ผ + example: "SIMPLE" + + url: + type: string + format: uri + description: CDN ์ด๋ฏธ์ง€ URL (Azure Blob Storage) + example: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + + platform: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + description: ํ”Œ๋žซํผ + example: "INSTAGRAM" + + size: + type: object + required: + - width + - height + properties: + width: + type: integer + description: ์ด๋ฏธ์ง€ ๋„ˆ๋น„ (ํ”ฝ์…€) + example: 1080 + height: + type: integer + description: ์ด๋ฏธ์ง€ ๋†’์ด (ํ”ฝ์…€) + example: 1080 + + createdAt: + type: string + format: date-time + description: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ๊ฐ + example: "2025-10-22T14:30:05Z" + + fallbackUsed: + type: boolean + description: Fallback ์ด๋ฏธ์ง€ ์‚ฌ์šฉ ์—ฌ๋ถ€ + example: false + + ImageRegenerationRequest: + type: object + description: ์ด๋ฏธ์ง€ ์žฌ์ƒ์„ฑ ์š”์ฒญ (์ฝ˜ํ…์ธ  ํŽธ์ง‘) + properties: + content: + type: string + description: ์ˆ˜์ •๋œ ํ”„๋กฌํ”„ํŠธ (์„ ํƒ์‚ฌํ•ญ) + example: "๋ฐ์€ ๋ถ„์œ„๊ธฐ๋กœ ๋ณ€๊ฒฝ" + + style: + type: string + description: ๋ณ€๊ฒฝํ•  ์Šคํƒ€์ผ (์„ ํƒ์‚ฌํ•ญ) + enum: [SIMPLE, FANCY, TRENDY] + example: "FANCY" + + CircuitBreakerErrorResponse: + type: object + description: Circuit Breaker ์˜ค๋ฅ˜ ์‘๋‹ต (์™ธ๋ถ€ API ์žฅ์• ) + required: + - code + - message + - timestamp + - circuitBreakerState + properties: + code: + type: string + description: ์—๋Ÿฌ ์ฝ”๋“œ + example: "IMAGE_GENERATION_SERVICE_UNAVAILABLE" + + message: + type: string + description: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + example: "์ด๋ฏธ์ง€ ์ƒ์„ฑ ์„œ๋น„์Šค๊ฐ€ ์ผ์‹œ์ ์œผ๋กœ ์‚ฌ์šฉ ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" + + timestamp: + type: string + format: date-time + description: ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ๊ฐ + example: "2025-10-22T14:30:00Z" + + circuitBreakerState: + type: string + enum: [OPEN, HALF_OPEN, CLOSED] + description: Circuit Breaker ์ƒํƒœ + example: "OPEN" + + fallbackAvailable: + type: boolean + description: Fallback ์ด๋ฏธ์ง€ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ + example: true + + fallbackImageUrl: + type: string + format: uri + description: Fallback ํ…œํ”Œ๋ฆฟ ์ด๋ฏธ์ง€ URL (์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ) + example: "https://cdn.kt-event.com/templates/default_event.png" + + retryAfter: + type: integer + description: ์žฌ์‹œ๋„ ๊ฐ€๋Šฅ ์‹œ๊ฐ„ (์ดˆ) + example: 300 diff --git a/design/backend/api/distribution-service-api.yaml b/design/backend/api/distribution-service-api.yaml new file mode 100644 index 0000000..938b3a8 --- /dev/null +++ b/design/backend/api/distribution-service-api.yaml @@ -0,0 +1,651 @@ +openapi: 3.0.3 +info: + title: Distribution Service API + description: | + KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค์˜ ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ ๊ด€๋ฆฌ API + + ## ์ฃผ์š” ๊ธฐ๋Šฅ + - ๋‹ค์ค‘ ์ฑ„๋„ ๋™์‹œ ๋ฐฐํฌ (์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS) + - ๋ฐฐํฌ ์ƒํƒœ ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง + - Circuit Breaker ๊ธฐ๋ฐ˜ ์žฅ์•  ๊ฒฉ๋ฆฌ + - Retry ํŒจํ„ด ๋ฐ Fallback ์ฒ˜๋ฆฌ + + ## ๋ฐฐํฌ ์ฑ„๋„ + - **์šฐ๋ฆฌ๋™๋„คTV**: ์˜์ƒ ์ฝ˜ํ…์ธ  ์—…๋กœ๋“œ + - **๋ง๊ณ ๋น„์ฆˆ**: ์—ฐ๊ฒฐ์Œ ์—…๋ฐ์ดํŠธ + - **์ง€๋‹ˆTV**: ๊ด‘๊ณ  ๋“ฑ๋ก + - **SNS**: Instagram, Naver Blog, Kakao Channel + + ## Resilience ํŒจํ„ด + - Circuit Breaker: ์ฑ„๋„๋ณ„ ๋…๋ฆฝ์  ์žฅ์•  ๊ฒฉ๋ฆฌ + - Retry: ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ (1s, 2s, 4s) ์ตœ๋Œ€ 3ํšŒ + - Bulkhead: ๋ฆฌ์†Œ์Šค ๊ฒฉ๋ฆฌ + - Fallback: ์‹คํŒจ ์ฑ„๋„ ์Šคํ‚ต ๋ฐ ์•Œ๋ฆผ + + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8085 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/distribution/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/distribution/v1 + description: Production Server + +tags: + - name: Distribution + description: ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ ๊ด€๋ฆฌ + - name: Monitoring + description: ๋ฐฐํฌ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง + +paths: + /distribution/distribute: + post: + tags: + - Distribution + summary: ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ ์š”์ฒญ + description: | + ์ด๋ฒคํŠธ ์ฝ˜ํ…์ธ ๋ฅผ ์„ ํƒ๋œ ์ฑ„๋„๋“ค์— ๋™์‹œ ๋ฐฐํฌํ•ฉ๋‹ˆ๋‹ค. + + ## ์ฒ˜๋ฆฌ ํ๋ฆ„ + 1. ๋ฐฐํฌ ์š”์ฒญ ๊ฒ€์ฆ (์ด๋ฒคํŠธ ID, ์ฑ„๋„ ๋ชฉ๋ก, ์ฝ˜ํ…์ธ  ๋ฐ์ดํ„ฐ) + 2. ์ฑ„๋„๋ณ„ ๋ณ‘๋ ฌ ๋ฐฐํฌ ์‹คํ–‰ (1๋ถ„ ์ด๋‚ด ์™„๋ฃŒ ๋ชฉํ‘œ) + 3. Circuit Breaker๋กœ ์žฅ์•  ์ฑ„๋„ ๊ฒฉ๋ฆฌ + 4. ์‹คํŒจ ์‹œ Retry (์ง€์ˆ˜ ๋ฐฑ์˜คํ”„: 1s, 2s, 4s) + 5. Fallback: ์‹คํŒจ ์ฑ„๋„ ์Šคํ‚ต ๋ฐ ์•Œ๋ฆผ + 6. ๋ฐฐํฌ ๊ฒฐ๊ณผ ์ง‘๊ณ„ ๋ฐ ๋กœ๊ทธ ์ €์žฅ + 7. DistributionCompleted ์ด๋ฒคํŠธ Kafka ๋ฐœํ–‰ + + ## Resilience ์ฒ˜๋ฆฌ + - ๊ฐ ์ฑ„๋„๋ณ„ ๋…๋ฆฝ์ ์ธ Circuit Breaker ์ ์šฉ + - ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„ (์ง€์ˆ˜ ๋ฐฑ์˜คํ”„) + - ์ผ๋ถ€ ์ฑ„๋„ ์‹คํŒจ ์‹œ์—๋„ ์„ฑ๊ณต ์ฑ„๋„์€ ์œ ์ง€ + - ์‹คํŒจ ์ฑ„๋„ ์ •๋ณด๋Š” ์‘๋‹ต์— ํฌํ•จ + + x-user-story: UFR-DIST-010 + x-controller: DistributionController + operationId: distributeToChannels + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionRequest' + examples: + multiChannel: + summary: ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ ์˜ˆ์‹œ + value: + eventId: "evt-12345" + channels: + - type: "WOORIDONGNE_TV" + config: + radius: "1km" + timeSlots: + - "weekday_evening" + - "weekend_lunch" + - type: "INSTAGRAM" + config: + scheduledTime: "2025-11-01T10:00:00Z" + - type: "NAVER_BLOG" + config: + scheduledTime: "2025-11-01T10:30:00Z" + contentUrls: + instagram: "https://cdn.example.com/images/event-instagram.jpg" + naverBlog: "https://cdn.example.com/images/event-naver.jpg" + kakaoChannel: "https://cdn.example.com/images/event-kakao.jpg" + responses: + '200': + description: ๋ฐฐํฌ ์™„๋ฃŒ + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionResponse' + examples: + allSuccess: + summary: ๋ชจ๋“  ์ฑ„๋„ ๋ฐฐํฌ ์„ฑ๊ณต + value: + distributionId: "dist-12345" + eventId: "evt-12345" + status: "COMPLETED" + completedAt: "2025-11-01T09:00:00Z" + results: + - channel: "WOORIDONGNE_TV" + status: "SUCCESS" + distributionId: "wtv-uuid-12345" + estimatedViews: 1000 + message: "๋ฐฐํฌ ์™„๋ฃŒ" + - channel: "INSTAGRAM" + status: "SUCCESS" + postUrl: "https://instagram.com/p/generated-post-id" + postId: "ig-post-12345" + message: "๊ฒŒ์‹œ ์™„๋ฃŒ" + - channel: "NAVER_BLOG" + status: "SUCCESS" + postUrl: "https://blog.naver.com/store123/generated-post" + message: "๊ฒŒ์‹œ ์™„๋ฃŒ" + '400': + description: ์ž˜๋ชป๋œ ์š”์ฒญ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidEventId: + summary: ์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฒคํŠธ ID + value: + error: "BAD_REQUEST" + message: "์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฒคํŠธ ID์ž…๋‹ˆ๋‹ค" + timestamp: "2025-11-01T09:00:00Z" + noChannels: + summary: ์„ ํƒ๋œ ์ฑ„๋„ ์—†์Œ + value: + error: "BAD_REQUEST" + message: "์ตœ์†Œ 1๊ฐœ ์ด์ƒ์˜ ์ฑ„๋„์„ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + timestamp: "2025-11-01T09:00:00Z" + '404': + description: ์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + eventNotFound: + summary: ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ด๋ฒคํŠธ + value: + error: "NOT_FOUND" + message: "์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: evt-12345" + timestamp: "2025-11-01T09:00:00Z" + '500': + description: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + internalError: + summary: ์„œ๋ฒ„ ์˜ค๋ฅ˜ + value: + error: "INTERNAL_SERVER_ERROR" + message: "๋ฐฐํฌ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค" + timestamp: "2025-11-01T09:00:00Z" + + /distribution/{eventId}/status: + get: + tags: + - Monitoring + summary: ๋ฐฐํฌ ์ƒํƒœ ์กฐํšŒ + description: | + ํŠน์ • ์ด๋ฒคํŠธ์˜ ๋ฐฐํฌ ์ƒํƒœ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + + ## ์กฐํšŒ ์ •๋ณด + - ์ „์ฒด ๋ฐฐํฌ ์ƒํƒœ (์ง„ํ–‰์ค‘, ์™„๋ฃŒ, ๋ถ€๋ถ„์„ฑ๊ณต, ์‹คํŒจ) + - ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ์ƒํƒœ ๋ฐ ๊ฒฐ๊ณผ + - ์‹คํŒจ ์ฑ„๋„ ์ƒ์„ธ ์ •๋ณด (์˜ค๋ฅ˜ ์œ ํ˜•, ์žฌ์‹œ๋„ ํšŸ์ˆ˜) + - ๋ฐฐํฌ ์‹œ์ž‘/์™„๋ฃŒ ์‹œ๊ฐ„ ๋ฐ ์†Œ์š” ์‹œ๊ฐ„ + - ์™ธ๋ถ€ ์ฑ„๋„ ID ๋ฐ ๋ฐฐํฌ URL + + ## ์ƒํƒœ ๊ฐ’ + - **IN_PROGRESS**: ๋ฐฐํฌ ์ง„ํ–‰ ์ค‘ + - **COMPLETED**: ๋ชจ๋“  ์ฑ„๋„ ๋ฐฐํฌ ์™„๋ฃŒ + - **PARTIAL_SUCCESS**: ์ผ๋ถ€ ์ฑ„๋„ ๋ฐฐํฌ ์„ฑ๊ณต + - **FAILED**: ๋ชจ๋“  ์ฑ„๋„ ๋ฐฐํฌ ์‹คํŒจ + + x-user-story: UFR-DIST-020 + x-controller: DistributionController + operationId: getDistributionStatus + parameters: + - name: eventId + in: path + required: true + description: ์ด๋ฒคํŠธ ID + schema: + type: string + example: "evt-12345" + responses: + '200': + description: ๋ฐฐํฌ ์ƒํƒœ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionStatusResponse' + examples: + completed: + summary: ๋ฐฐํฌ ์™„๋ฃŒ ์ƒํƒœ + value: + eventId: "evt-12345" + overallStatus: "COMPLETED" + completedAt: "2025-11-01T09:00:00Z" + channels: + - channel: "WOORIDONGNE_TV" + status: "COMPLETED" + distributionId: "wtv-uuid-12345" + estimatedViews: 1500 + completedAt: "2025-11-01T09:00:00Z" + - channel: "RINGO_BIZ" + status: "COMPLETED" + updateTimestamp: "2025-11-01T09:00:00Z" + - channel: "GENIE_TV" + status: "COMPLETED" + adId: "gtv-uuid-12345" + impressionSchedule: + - "2025-11-01 18:00-20:00" + - "2025-11-02 12:00-14:00" + - channel: "INSTAGRAM" + status: "COMPLETED" + postUrl: "https://instagram.com/p/generated-post-id" + postId: "ig-post-12345" + - channel: "NAVER_BLOG" + status: "COMPLETED" + postUrl: "https://blog.naver.com/store123/generated-post" + - channel: "KAKAO_CHANNEL" + status: "COMPLETED" + messageId: "kakao-msg-12345" + inProgress: + summary: ๋ฐฐํฌ ์ง„ํ–‰์ค‘ ์ƒํƒœ + value: + eventId: "evt-12345" + overallStatus: "IN_PROGRESS" + startedAt: "2025-11-01T08:58:00Z" + channels: + - channel: "WOORIDONGNE_TV" + status: "COMPLETED" + distributionId: "wtv-uuid-12345" + estimatedViews: 1500 + - channel: "INSTAGRAM" + status: "IN_PROGRESS" + progress: 50 + - channel: "NAVER_BLOG" + status: "PENDING" + partialFailure: + summary: ์ผ๋ถ€ ์ฑ„๋„ ์‹คํŒจ ์ƒํƒœ + value: + eventId: "evt-12345" + overallStatus: "PARTIAL_FAILURE" + completedAt: "2025-11-01T09:00:00Z" + channels: + - channel: "WOORIDONGNE_TV" + status: "COMPLETED" + distributionId: "wtv-uuid-12345" + estimatedViews: 1500 + - channel: "INSTAGRAM" + status: "FAILED" + errorMessage: "Instagram API ํƒ€์ž„์•„์›ƒ" + retries: 3 + lastRetryAt: "2025-11-01T08:59:30Z" + - channel: "NAVER_BLOG" + status: "COMPLETED" + postUrl: "https://blog.naver.com/store123/generated-post" + '404': + description: ๋ฐฐํฌ ์ด๋ ฅ์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: ๋ฐฐํฌ ์ด๋ ฅ ์—†์Œ + value: + error: "NOT_FOUND" + message: "๋ฐฐํฌ ์ด๋ ฅ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: evt-12345" + timestamp: "2025-11-01T09:00:00Z" + '500': + description: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +components: + schemas: + DistributionRequest: + type: object + required: + - eventId + - channels + - contentUrls + properties: + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt-12345" + channels: + type: array + description: ๋ฐฐํฌํ•  ์ฑ„๋„ ๋ชฉ๋ก + minItems: 1 + items: + $ref: '#/components/schemas/ChannelConfig' + contentUrls: + type: object + description: ํ”Œ๋žซํผ๋ณ„ ์ฝ˜ํ…์ธ  URL + properties: + wooridongneTV: + type: string + description: ์šฐ๋ฆฌ๋™๋„คTV ์˜์ƒ URL (15์ดˆ) + example: "https://cdn.example.com/videos/event-15s.mp4" + ringoBiz: + type: string + description: ๋ง๊ณ ๋น„์ฆˆ ์—ฐ๊ฒฐ์Œ ํŒŒ์ผ URL + example: "https://cdn.example.com/audio/ringtone.mp3" + genieTV: + type: string + description: ์ง€๋‹ˆTV ๊ด‘๊ณ  ์˜์ƒ URL + example: "https://cdn.example.com/videos/event-ad.mp4" + instagram: + type: string + description: Instagram ์ด๋ฏธ์ง€ URL (1080x1080) + example: "https://cdn.example.com/images/event-instagram.jpg" + naverBlog: + type: string + description: Naver Blog ์ด๋ฏธ์ง€ URL (800x600) + example: "https://cdn.example.com/images/event-naver.jpg" + kakaoChannel: + type: string + description: Kakao Channel ์ด๋ฏธ์ง€ URL (800x800) + example: "https://cdn.example.com/images/event-kakao.jpg" + + ChannelConfig: + type: object + required: + - type + properties: + type: + type: string + description: ์ฑ„๋„ ํƒ€์ž… + enum: + - WOORIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - INSTAGRAM + - NAVER_BLOG + - KAKAO_CHANNEL + example: "INSTAGRAM" + config: + type: object + description: ์ฑ„๋„๋ณ„ ์„ค์ • (์ฑ„๋„์— ๋”ฐ๋ผ ๋‹ค๋ฆ„) + additionalProperties: true + example: + scheduledTime: "2025-11-01T10:00:00Z" + caption: "์ด๋ฒคํŠธ ์•ˆ๋‚ด" + hashtags: + - "์ด๋ฒคํŠธ" + - "ํ• ์ธ" + + DistributionResponse: + type: object + required: + - distributionId + - eventId + - status + - results + properties: + distributionId: + type: string + description: ๋ฐฐํฌ ID + example: "dist-12345" + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt-12345" + status: + type: string + description: ์ „์ฒด ๋ฐฐํฌ ์ƒํƒœ + enum: + - PENDING + - IN_PROGRESS + - COMPLETED + - PARTIAL_FAILURE + - FAILED + example: "COMPLETED" + startedAt: + type: string + format: date-time + description: ๋ฐฐํฌ ์‹œ์ž‘ ์‹œ๊ฐ + example: "2025-11-01T08:59:00Z" + completedAt: + type: string + format: date-time + description: ๋ฐฐํฌ ์™„๋ฃŒ ์‹œ๊ฐ + example: "2025-11-01T09:00:00Z" + results: + type: array + description: ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ๊ฒฐ๊ณผ + items: + $ref: '#/components/schemas/ChannelResult' + + ChannelResult: + type: object + required: + - channel + - status + properties: + channel: + type: string + description: ์ฑ„๋„ ํƒ€์ž… + enum: + - WOORIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - INSTAGRAM + - NAVER_BLOG + - KAKAO_CHANNEL + example: "INSTAGRAM" + status: + type: string + description: ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ์ƒํƒœ + enum: + - PENDING + - IN_PROGRESS + - SUCCESS + - FAILED + example: "SUCCESS" + distributionId: + type: string + description: ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ID (์šฐ๋ฆฌ๋™๋„คTV, ์ง€๋‹ˆTV) + example: "wtv-uuid-12345" + estimatedViews: + type: integer + description: ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜ (์šฐ๋ฆฌ๋™๋„คTV, ์ง€๋‹ˆTV) + example: 1500 + updateTimestamp: + type: string + format: date-time + description: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ ์‹œ๊ฐ (๋ง๊ณ ๋น„์ฆˆ) + example: "2025-11-01T09:00:00Z" + adId: + type: string + description: ๊ด‘๊ณ  ID (์ง€๋‹ˆTV) + example: "gtv-uuid-12345" + impressionSchedule: + type: array + description: ๋…ธ์ถœ ์Šค์ผ€์ค„ (์ง€๋‹ˆTV) + items: + type: string + example: + - "2025-11-01 18:00-20:00" + - "2025-11-02 12:00-14:00" + postUrl: + type: string + description: ๊ฒŒ์‹œ๋ฌผ URL (Instagram, Naver Blog) + example: "https://instagram.com/p/generated-post-id" + postId: + type: string + description: ๊ฒŒ์‹œ๋ฌผ ID (Instagram) + example: "ig-post-12345" + messageId: + type: string + description: ๋ฉ”์‹œ์ง€ ID (Kakao Channel) + example: "kakao-msg-12345" + message: + type: string + description: ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + example: "๋ฐฐํฌ ์™„๋ฃŒ" + errorMessage: + type: string + description: ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ (์‹คํŒจ ์‹œ) + example: "Instagram API ํƒ€์ž„์•„์›ƒ" + retries: + type: integer + description: ์žฌ์‹œ๋„ ํšŸ์ˆ˜ + example: 0 + lastRetryAt: + type: string + format: date-time + description: ๋งˆ์ง€๋ง‰ ์žฌ์‹œ๋„ ์‹œ๊ฐ + example: "2025-11-01T08:59:30Z" + + DistributionStatusResponse: + type: object + required: + - eventId + - overallStatus + - channels + properties: + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt-12345" + overallStatus: + type: string + description: ์ „์ฒด ๋ฐฐํฌ ์ƒํƒœ + enum: + - PENDING + - IN_PROGRESS + - COMPLETED + - PARTIAL_FAILURE + - FAILED + - NOT_FOUND + example: "COMPLETED" + startedAt: + type: string + format: date-time + description: ๋ฐฐํฌ ์‹œ์ž‘ ์‹œ๊ฐ + example: "2025-11-01T08:59:00Z" + completedAt: + type: string + format: date-time + description: ๋ฐฐํฌ ์™„๋ฃŒ ์‹œ๊ฐ + example: "2025-11-01T09:00:00Z" + channels: + type: array + description: ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ์ƒํƒœ + items: + $ref: '#/components/schemas/ChannelStatus' + + ChannelStatus: + type: object + required: + - channel + - status + properties: + channel: + type: string + description: ์ฑ„๋„ ํƒ€์ž… + enum: + - WOORIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - INSTAGRAM + - NAVER_BLOG + - KAKAO_CHANNEL + example: "INSTAGRAM" + status: + type: string + description: ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ์ƒํƒœ + enum: + - PENDING + - IN_PROGRESS + - COMPLETED + - FAILED + example: "COMPLETED" + progress: + type: integer + description: ์ง„ํ–‰๋ฅ  (0-100, IN_PROGRESS ์ƒํƒœ์ผ ๋•Œ) + minimum: 0 + maximum: 100 + example: 75 + distributionId: + type: string + description: ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ID + example: "wtv-uuid-12345" + estimatedViews: + type: integer + description: ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜ + example: 1500 + updateTimestamp: + type: string + format: date-time + description: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ ์‹œ๊ฐ + example: "2025-11-01T09:00:00Z" + adId: + type: string + description: ๊ด‘๊ณ  ID + example: "gtv-uuid-12345" + impressionSchedule: + type: array + description: ๋…ธ์ถœ ์Šค์ผ€์ค„ + items: + type: string + example: + - "2025-11-01 18:00-20:00" + postUrl: + type: string + description: ๊ฒŒ์‹œ๋ฌผ URL + example: "https://instagram.com/p/generated-post-id" + postId: + type: string + description: ๊ฒŒ์‹œ๋ฌผ ID + example: "ig-post-12345" + messageId: + type: string + description: ๋ฉ”์‹œ์ง€ ID + example: "kakao-msg-12345" + completedAt: + type: string + format: date-time + description: ์™„๋ฃŒ ์‹œ๊ฐ + example: "2025-11-01T09:00:00Z" + errorMessage: + type: string + description: ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ + example: "Instagram API ํƒ€์ž„์•„์›ƒ" + retries: + type: integer + description: ์žฌ์‹œ๋„ ํšŸ์ˆ˜ + example: 3 + lastRetryAt: + type: string + format: date-time + description: ๋งˆ์ง€๋ง‰ ์žฌ์‹œ๋„ ์‹œ๊ฐ + example: "2025-11-01T08:59:30Z" + + ErrorResponse: + type: object + required: + - error + - message + - timestamp + properties: + error: + type: string + description: ์˜ค๋ฅ˜ ์ฝ”๋“œ + enum: + - BAD_REQUEST + - NOT_FOUND + - INTERNAL_SERVER_ERROR + example: "BAD_REQUEST" + message: + type: string + description: ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ + example: "์œ ํšจํ•˜์ง€ ์•Š์€ ์ด๋ฒคํŠธ ID์ž…๋‹ˆ๋‹ค" + timestamp: + type: string + format: date-time + description: ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ๊ฐ + example: "2025-11-01T09:00:00Z" + details: + type: object + description: ์ถ”๊ฐ€ ์˜ค๋ฅ˜ ์ •๋ณด (์„ ํƒ ์‚ฌํ•ญ) + additionalProperties: true diff --git a/design/backend/api/event-service-api.yaml b/design/backend/api/event-service-api.yaml new file mode 100644 index 0000000..c179b53 --- /dev/null +++ b/design/backend/api/event-service-api.yaml @@ -0,0 +1,1384 @@ +openapi: 3.0.3 +info: + title: Event Service API + description: | + KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - Event Service API + + ์ด๋ฒคํŠธ ์ „์ฒด ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ (์ƒ์„ฑ, ์กฐํšŒ, ์ˆ˜์ •, ๋ฐฐํฌ, ์ข…๋ฃŒ) + - AI ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ถ”์ฒœ ๋ฐ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• + - ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ฐ ํŽธ์ง‘ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ + - ๋ฐฐํฌ ์ฑ„๋„ ๊ด€๋ฆฌ ๋ฐ ์ตœ์ข… ๋ฐฐํฌ + - ์ด๋ฒคํŠธ ์ƒํƒœ ๊ด€๋ฆฌ (DRAFT, PUBLISHED, ENDED) + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8080 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/event/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/event/v1 + description: Production Server + +security: + - BearerAuth: [] + +tags: + - name: Dashboard + description: ๋Œ€์‹œ๋ณด๋“œ ๋ฐ ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ + - name: Event Creation + description: ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ + - name: Event Management + description: ์ด๋ฒคํŠธ ์ˆ˜์ •, ์‚ญ์ œ, ์ข…๋ฃŒ + - name: Job Status + description: ๋น„๋™๊ธฐ ์ž‘์—… ์ƒํƒœ ์กฐํšŒ + +paths: + /events: + get: + tags: + - Dashboard + summary: ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ + description: | + ์‚ฌ์šฉ์ž์˜ ์ด๋ฒคํŠธ ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค (๋Œ€์‹œ๋ณด๋“œ, ์ „์ฒด๋ณด๊ธฐ). + ํ•„ํ„ฐ, ๊ฒ€์ƒ‰, ํŽ˜์ด์ง•์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + operationId: getEvents + x-user-story: UFR-EVENT-010, UFR-EVENT-070 + x-controller: EventController.getEvents + parameters: + - name: status + in: query + description: ์ด๋ฒคํŠธ ์ƒํƒœ ํ•„ํ„ฐ (DRAFT, PUBLISHED, ENDED) + required: false + schema: + type: string + enum: [DRAFT, PUBLISHED, ENDED] + example: PUBLISHED + - name: objective + in: query + description: ์ด๋ฒคํŠธ ๋ชฉ์  ํ•„ํ„ฐ + required: false + schema: + type: string + example: ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜ + - name: search + in: query + description: ๊ฒ€์ƒ‰์–ด (์ด๋ฒคํŠธ๋ช…) + required: false + schema: + type: string + example: ๋ด„๋งž์ด + - name: page + in: query + description: ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘) + required: false + schema: + type: integer + minimum: 0 + default: 0 + example: 0 + - name: size + in: query + description: ํŽ˜์ด์ง€ ํฌ๊ธฐ + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + example: 20 + - name: sort + in: query + description: ์ •๋ ฌ ๊ธฐ์ค€ (createdAt, startDate, endDate) + required: false + schema: + type: string + enum: [createdAt, startDate, endDate] + default: createdAt + example: createdAt + - name: order + in: query + description: ์ •๋ ฌ ์ˆœ์„œ (asc, desc) + required: false + schema: + type: string + enum: [asc, desc] + default: desc + example: desc + responses: + '200': + description: ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/EventListResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}: + get: + tags: + - Dashboard + summary: ์ด๋ฒคํŠธ ์ƒ์„ธ ์กฐํšŒ + description: ํŠน์ • ์ด๋ฒคํŠธ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + operationId: getEvent + x-user-story: UFR-EVENT-060 + x-controller: EventController.getEvent + parameters: + - $ref: '#/components/parameters/EventId' + responses: + '200': + description: ์ด๋ฒคํŠธ ์ƒ์„ธ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + put: + tags: + - Event Management + summary: ์ด๋ฒคํŠธ ์ˆ˜์ • + description: | + ๊ธฐ์กด ์ด๋ฒคํŠธ์˜ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. + DRAFT ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋งŒ ์ „์ฒด ์ˆ˜์ • ๊ฐ€๋Šฅํ•˜๋ฉฐ, + PUBLISHED ์ƒํƒœ์—์„œ๋Š” ์ œํ•œ์  ์ˆ˜์ •๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + operationId: updateEvent + x-user-story: UFR-EVENT-060 + x-controller: EventController.updateEvent + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateEventRequest' + responses: + '200': + description: ์ด๋ฒคํŠธ ์ˆ˜์ • ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: ์ด๋ฒคํŠธ ์ƒํƒœ๋กœ ์ธํ•ด ์ˆ˜์ • ๋ถˆ๊ฐ€ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_MODIFIABLE + message: PUBLISHED ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋Š” ์ œํ•œ์ ์œผ๋กœ๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + '500': + $ref: '#/components/responses/InternalServerError' + + delete: + tags: + - Event Management + summary: ์ด๋ฒคํŠธ ์‚ญ์ œ + description: | + ์ด๋ฒคํŠธ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. + DRAFT ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋งŒ ์‚ญ์ œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + operationId: deleteEvent + x-user-story: UFR-EVENT-070 + x-controller: EventController.deleteEvent + parameters: + - $ref: '#/components/parameters/EventId' + responses: + '204': + description: ์ด๋ฒคํŠธ ์‚ญ์ œ ์„ฑ๊ณต + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: ์ด๋ฒคํŠธ ์ƒํƒœ๋กœ ์ธํ•ด ์‚ญ์ œ ๋ถˆ๊ฐ€ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_DELETABLE + message: DRAFT ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋งŒ ์‚ญ์ œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/objectives: + post: + tags: + - Event Creation + summary: ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ (Step 1) + description: | + ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ์˜ ์ฒซ ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค. + ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฒคํŠธ ๋ชฉ์ ์„ ์„ ํƒํ•˜๊ณ  DRAFT ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + operationId: selectObjective + x-user-story: UFR-EVENT-020 + x-controller: EventController.selectObjective + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SelectObjectiveRequest' + responses: + '201': + description: ์ด๋ฒคํŠธ ์ƒ์„ฑ ์„ฑ๊ณต (DRAFT ์ƒํƒœ) + content: + application/json: + schema: + $ref: '#/components/schemas/EventCreatedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/ai-recommendations: + post: + tags: + - Event Creation + summary: AI ์ถ”์ฒœ ์š”์ฒญ (Step 2) + description: | + AI ์„œ๋น„์Šค์— ์ด๋ฒคํŠธ ์ถ”์ฒœ ์ƒ์„ฑ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. + Kafka Job์„ ๋ฐœํ–‰ํ•˜๊ณ  jobId๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + Job ์ƒํƒœ๋Š” /jobs/{jobId}๋กœ ํด๋งํ•˜์—ฌ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + operationId: requestAiRecommendations + x-user-story: UFR-EVENT-030 + x-controller: EventController.requestAiRecommendations + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AiRecommendationRequest' + responses: + '202': + description: AI ์ถ”์ฒœ ์š”์ฒญ ์ ‘์ˆ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/JobAcceptedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: ์ด๋ฒคํŠธ ์ƒํƒœ๊ฐ€ ์ ์ ˆํ•˜์ง€ ์•Š์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: DRAFT ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋งŒ AI ์ถ”์ฒœ์„ ์š”์ฒญํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/recommendations: + put: + tags: + - Event Creation + summary: AI ์ถ”์ฒœ ์„ ํƒ ๋ฐ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• (Step 2-2) + description: | + AI๊ฐ€ ์ƒ์„ฑํ•œ ์ถ”์ฒœ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•˜๊ณ , + ํ•„์š”์‹œ ์ด๋ฒคํŠธ๋ช…, ๋ฌธ๊ตฌ, ๊ธฐ๊ฐ„ ๋“ฑ์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•ฉ๋‹ˆ๋‹ค. + operationId: selectRecommendation + x-user-story: UFR-EVENT-030 + x-controller: EventController.selectRecommendation + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SelectRecommendationRequest' + responses: + '200': + description: AI ์ถ”์ฒœ ์„ ํƒ ๋ฐ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: ์ด๋ฒคํŠธ ์ƒํƒœ๊ฐ€ ์ ์ ˆํ•˜์ง€ ์•Š์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: AI ์ถ”์ฒœ์ด ์™„๋ฃŒ๋œ DRAFT ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋งŒ ์„ ํƒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/images: + post: + tags: + - Event Creation + summary: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ (Step 3) + description: | + Content Service์— ์ด๋ฏธ์ง€ ์ƒ์„ฑ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค. + Kafka Job์„ ๋ฐœํ–‰ํ•˜๊ณ  jobId๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + Job ์ƒํƒœ๋Š” /jobs/{jobId}๋กœ ํด๋งํ•˜์—ฌ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + operationId: requestImageGeneration + x-user-story: UFR-CONT-010 + x-controller: EventController.requestImageGeneration + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImageGenerationRequest' + responses: + '202': + description: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ ์ ‘์ˆ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/JobAcceptedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: ์ด๋ฒคํŠธ ์ƒํƒœ๊ฐ€ ์ ์ ˆํ•˜์ง€ ์•Š์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: AI ์ถ”์ฒœ์ด ์„ ํƒ๋œ DRAFT ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋งŒ ์ด๋ฏธ์ง€ ์ƒ์„ฑ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/images/{imageId}/select: + put: + tags: + - Event Creation + summary: ์ด๋ฏธ์ง€ ์„ ํƒ (Step 3-2) + description: | + ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. + ์„ ํƒ๋œ ์ด๋ฏธ์ง€๋Š” ์ด๋ฒคํŠธ์˜ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€๋กœ ์„ค์ •๋ฉ๋‹ˆ๋‹ค. + operationId: selectImage + x-user-story: UFR-CONT-010 + x-controller: EventController.selectImage + parameters: + - $ref: '#/components/parameters/EventId' + - name: imageId + in: path + description: ์ด๋ฏธ์ง€ ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440006" + responses: + '200': + description: ์ด๋ฏธ์ง€ ์„ ํƒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: ์ด๋ฒคํŠธ ๋˜๋Š” ์ด๋ฏธ์ง€ ์ƒํƒœ๊ฐ€ ์ ์ ˆํ•˜์ง€ ์•Š์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_IMAGE_STATE + message: ํ•ด๋‹น ์ด๋ฏธ์ง€๋Š” ์ด ์ด๋ฒคํŠธ์— ์†ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/images/{imageId}/edit: + put: + tags: + - Event Creation + summary: ์ด๋ฏธ์ง€ ํŽธ์ง‘ (Step 3-3) + description: | + ์„ ํƒ๋œ ์ด๋ฏธ์ง€๋ฅผ ํŽธ์ง‘ํ•ฉ๋‹ˆ๋‹ค. + Content Service์— ํŽธ์ง‘ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ์ƒˆ๋กœ์šด ์ด๋ฏธ์ง€ URL์„ ๋ฐ›์Šต๋‹ˆ๋‹ค. + operationId: editImage + x-user-story: UFR-CONT-020 + x-controller: EventController.editImage + parameters: + - $ref: '#/components/parameters/EventId' + - name: imageId + in: path + description: ์ด๋ฏธ์ง€ ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440006" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImageEditRequest' + responses: + '200': + description: ์ด๋ฏธ์ง€ ํŽธ์ง‘ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/ImageEditResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: ์ด๋ฏธ์ง€ ์ƒํƒœ๊ฐ€ ์ ์ ˆํ•˜์ง€ ์•Š์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: IMAGE_NOT_EDITABLE + message: ์„ ํƒ๋œ ์ด๋ฏธ์ง€๋งŒ ํŽธ์ง‘ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/channels: + put: + tags: + - Event Creation + summary: ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ (Step 4) + description: | + ์ด๋ฒคํŠธ๋ฅผ ๋ฐฐํฌํ•  ์ฑ„๋„์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. + (์›น์‚ฌ์ดํŠธ, ์นด์นด์˜คํ†ก, Instagram, Facebook ๋“ฑ) + operationId: selectChannels + x-user-story: UFR-EVENT-040 + x-controller: EventController.selectChannels + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SelectChannelsRequest' + responses: + '200': + description: ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: ์ด๋ฒคํŠธ ์ƒํƒœ๊ฐ€ ์ ์ ˆํ•˜์ง€ ์•Š์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: ์ด๋ฏธ์ง€๊ฐ€ ์„ ํƒ๋œ DRAFT ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋งŒ ์ฑ„๋„ ์„ ํƒ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + '500': + $ref: '#/components/responses/InternalServerError' + + /events/{eventId}/publish: + post: + tags: + - Event Creation + summary: ์ตœ์ข… ์Šน์ธ ๋ฐ ๋ฐฐํฌ (Step 5) + description: | + ์ด๋ฒคํŠธ๋ฅผ ์ตœ์ข… ์Šน์ธํ•˜๊ณ  ์„ ํƒ๋œ ์ฑ„๋„์— ๋ฐฐํฌํ•ฉ๋‹ˆ๋‹ค. + Distribution Service๋ฅผ ๋™๊ธฐ ํ˜ธ์ถœํ•˜์—ฌ ๋ฐฐํฌํ•˜๊ณ , + ์ด๋ฒคํŠธ ์ƒํƒœ๋ฅผ PUBLISHED๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. + Kafka Event (EventCreated)๋ฅผ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค. + operationId: publishEvent + x-user-story: UFR-EVENT-050 + x-controller: EventController.publishEvent + parameters: + - $ref: '#/components/parameters/EventId' + responses: + '200': + description: ์ด๋ฒคํŠธ ๋ฐฐํฌ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/EventPublishedResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: ์ด๋ฒคํŠธ ์ƒํƒœ๊ฐ€ ์ ์ ˆํ•˜์ง€ ์•Š์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_PUBLISHABLE + message: ๋ฐฐํฌ ์ฑ„๋„์ด ์„ ํƒ๋œ DRAFT ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋งŒ ๋ฐฐํฌ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + '500': + $ref: '#/components/responses/InternalServerError' + '503': + description: Distribution Service ํ˜ธ์ถœ ์‹คํŒจ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: DISTRIBUTION_SERVICE_UNAVAILABLE + message: ๋ฐฐํฌ ์„œ๋น„์Šค๋ฅผ ์ผ์‹œ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + /events/{eventId}/end: + post: + tags: + - Event Management + summary: ์ด๋ฒคํŠธ ์กฐ๊ธฐ ์ข…๋ฃŒ + description: | + ์ง„ํ–‰ ์ค‘์ธ ์ด๋ฒคํŠธ๋ฅผ ์กฐ๊ธฐ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค. + PUBLISHED ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋งŒ ์ข…๋ฃŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ, + ์ข…๋ฃŒ ์‹œ ์ƒํƒœ๊ฐ€ ENDED๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. + operationId: endEvent + x-user-story: UFR-EVENT-060 + x-controller: EventController.endEvent + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EndEventRequest' + responses: + '200': + description: ์ด๋ฒคํŠธ ์ข…๋ฃŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: ์ด๋ฒคํŠธ ์ƒํƒœ๋กœ ์ธํ•ด ์ข…๋ฃŒ ๋ถˆ๊ฐ€ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_ENDABLE + message: PUBLISHED ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋งŒ ์ข…๋ฃŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + '500': + $ref: '#/components/responses/InternalServerError' + + /jobs/{jobId}: + get: + tags: + - Job Status + summary: Job ์ƒํƒœ ํด๋ง + description: | + ๋น„๋™๊ธฐ ์ž‘์—…(AI ์ถ”์ฒœ ์ƒ์„ฑ, ์ด๋ฏธ์ง€ ์ƒ์„ฑ)์˜ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + ํด๋ผ์ด์–ธํŠธ๋Š” COMPLETED ๋˜๋Š” FAILED๊ฐ€ ๋  ๋•Œ๊นŒ์ง€ ํด๋งํ•ฉ๋‹ˆ๋‹ค. + COMPLETED ์‹œ Redis์—์„œ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + operationId: getJobStatus + x-user-story: UFR-EVENT-030, UFR-CONT-010 + x-controller: JobController.getJobStatus + parameters: + - name: jobId + in: path + description: Job ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440005" + responses: + '200': + description: Job ์ƒํƒœ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/JobStatusResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT Bearer ํ† ํฐ ์ธ์ฆ + + **ํ˜•์‹:** Authorization: Bearer {JWT_TOKEN} + + **ํ† ํฐ ๋งŒ๋ฃŒ:** 7์ผ + + **Claims:** + - userId: ์‚ฌ์šฉ์ž ID + - role: ์‚ฌ์šฉ์ž ์—ญํ•  (OWNER) + - iat: ๋ฐœ๊ธ‰ ์‹œ๊ฐ + - exp: ๋งŒ๋ฃŒ ์‹œ๊ฐ + + parameters: + EventId: + name: eventId + in: path + description: ์ด๋ฒคํŠธ ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440000" + + schemas: + EventListResponse: + type: object + properties: + content: + type: array + items: + $ref: '#/components/schemas/EventSummary' + page: + $ref: '#/components/schemas/PageInfo' + required: + - content + - page + + EventSummary: + type: object + properties: + eventId: + type: string + format: uuid + description: ์ด๋ฒคํŠธ ID + example: "550e8400-e29b-41d4-a716-446655440000" + eventName: + type: string + description: ์ด๋ฒคํŠธ๋ช… + example: "๋ด„๋งž์ด 20% ํ• ์ธ ์ด๋ฒคํŠธ" + objective: + type: string + description: ์ด๋ฒคํŠธ ๋ชฉ์  + example: "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜" + status: + type: string + enum: [DRAFT, PUBLISHED, ENDED] + description: ์ด๋ฒคํŠธ ์ƒํƒœ + example: "PUBLISHED" + startDate: + type: string + format: date + description: ์‹œ์ž‘์ผ + example: "2025-03-01" + endDate: + type: string + format: date + description: ์ข…๋ฃŒ์ผ + example: "2025-03-31" + thumbnailUrl: + type: string + format: uri + description: ์ธ๋„ค์ผ ์ด๋ฏธ์ง€ URL + example: "https://cdn.kt-event.com/images/event-thumb-001.jpg" + createdAt: + type: string + format: date-time + description: ์ƒ์„ฑ์ผ์‹œ + example: "2025-02-15T10:30:00Z" + required: + - eventId + - eventName + - objective + - status + - startDate + - endDate + - createdAt + + EventDetailResponse: + type: object + properties: + eventId: + type: string + format: uuid + description: ์ด๋ฒคํŠธ ID + example: "550e8400-e29b-41d4-a716-446655440000" + userId: + type: string + format: uuid + description: ์‚ฌ์šฉ์ž ID + example: "550e8400-e29b-41d4-a716-446655440001" + storeId: + type: string + format: uuid + description: ๋งค์žฅ ID + example: "550e8400-e29b-41d4-a716-446655440002" + eventName: + type: string + description: ์ด๋ฒคํŠธ๋ช… + example: "๋ด„๋งž์ด 20% ํ• ์ธ ์ด๋ฒคํŠธ" + objective: + type: string + description: ์ด๋ฒคํŠธ ๋ชฉ์  + example: "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜" + description: + type: string + description: ์ด๋ฒคํŠธ ์„ค๋ช… + example: "๋ด„์„ ๋งž์ดํ•˜์—ฌ ๋ชจ๋“  ์ƒํ’ˆ 20% ํ• ์ธ ํ–‰์‚ฌ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค." + targetAudience: + type: string + description: ํƒ€๊ฒŸ ๊ณ ๊ฐ + example: "20-30๋Œ€ ์—ฌ์„ฑ" + promotionType: + type: string + description: ํ”„๋กœ๋ชจ์…˜ ์œ ํ˜• + example: "ํ• ์ธ" + discountRate: + type: integer + description: ํ• ์ธ์œจ (%) + example: 20 + startDate: + type: string + format: date + description: ์‹œ์ž‘์ผ + example: "2025-03-01" + endDate: + type: string + format: date + description: ์ข…๋ฃŒ์ผ + example: "2025-03-31" + status: + type: string + enum: [DRAFT, PUBLISHED, ENDED] + description: ์ด๋ฒคํŠธ ์ƒํƒœ + example: "PUBLISHED" + selectedImageId: + type: string + format: uuid + description: ์„ ํƒ๋œ ์ด๋ฏธ์ง€ ID + example: "550e8400-e29b-41d4-a716-446655440006" + selectedImageUrl: + type: string + format: uri + description: ์„ ํƒ๋œ ์ด๋ฏธ์ง€ URL + example: "https://cdn.kt-event.com/images/event-img-001.jpg" + generatedImages: + type: array + description: ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ๋ชฉ๋ก + items: + $ref: '#/components/schemas/GeneratedImage' + channels: + type: array + description: ๋ฐฐํฌ ์ฑ„๋„ ๋ชฉ๋ก + items: + type: string + example: "WEBSITE" + aiRecommendations: + type: array + description: AI ์ถ”์ฒœ ๋ชฉ๋ก + items: + $ref: '#/components/schemas/AiRecommendation' + createdAt: + type: string + format: date-time + description: ์ƒ์„ฑ์ผ์‹œ + example: "2025-02-15T10:30:00Z" + updatedAt: + type: string + format: date-time + description: ์ˆ˜์ •์ผ์‹œ + example: "2025-02-20T14:45:00Z" + required: + - eventId + - userId + - storeId + - eventName + - objective + - status + - startDate + - endDate + - createdAt + + GeneratedImage: + type: object + properties: + imageId: + type: string + format: uuid + description: ์ด๋ฏธ์ง€ ID + example: "550e8400-e29b-41d4-a716-446655440006" + imageUrl: + type: string + format: uri + description: ์ด๋ฏธ์ง€ URL + example: "https://cdn.kt-event.com/images/event-img-001.jpg" + isSelected: + type: boolean + description: ์„ ํƒ ์—ฌ๋ถ€ + example: true + createdAt: + type: string + format: date-time + description: ์ƒ์„ฑ์ผ์‹œ + example: "2025-02-16T11:00:00Z" + required: + - imageId + - imageUrl + - isSelected + - createdAt + + AiRecommendation: + type: object + properties: + recommendationId: + type: string + format: uuid + description: ์ถ”์ฒœ ID + example: "550e8400-e29b-41d4-a716-446655440007" + eventName: + type: string + description: ์ถ”์ฒœ ์ด๋ฒคํŠธ๋ช… + example: "๋ด„๋งž์ด 20% ํ• ์ธ ์ด๋ฒคํŠธ" + description: + type: string + description: ์ถ”์ฒœ ์„ค๋ช… + example: "๋ด„์„ ๋งž์ดํ•˜์—ฌ ๋ชจ๋“  ์ƒํ’ˆ 20% ํ• ์ธ ํ–‰์‚ฌ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค." + promotionType: + type: string + description: ์ถ”์ฒœ ํ”„๋กœ๋ชจ์…˜ ์œ ํ˜• + example: "ํ• ์ธ" + targetAudience: + type: string + description: ์ถ”์ฒœ ํƒ€๊ฒŸ ๊ณ ๊ฐ + example: "20-30๋Œ€ ์—ฌ์„ฑ" + isSelected: + type: boolean + description: ์„ ํƒ ์—ฌ๋ถ€ + example: true + required: + - recommendationId + - eventName + - description + - isSelected + + SelectObjectiveRequest: + type: object + properties: + objective: + type: string + description: ์ด๋ฒคํŠธ ๋ชฉ์  + example: "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜" + required: + - objective + + EventCreatedResponse: + type: object + properties: + eventId: + type: string + format: uuid + description: ์ƒ์„ฑ๋œ ์ด๋ฒคํŠธ ID + example: "550e8400-e29b-41d4-a716-446655440000" + status: + type: string + enum: [DRAFT] + description: ์ด๋ฒคํŠธ ์ƒํƒœ (ํ•ญ์ƒ DRAFT) + example: "DRAFT" + objective: + type: string + description: ์„ ํƒ๋œ ์ด๋ฒคํŠธ ๋ชฉ์  + example: "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜" + createdAt: + type: string + format: date-time + description: ์ƒ์„ฑ์ผ์‹œ + example: "2025-02-15T10:30:00Z" + required: + - eventId + - status + - objective + - createdAt + + AiRecommendationRequest: + type: object + properties: + storeInfo: + type: object + description: ๋งค์žฅ ์ •๋ณด (User Service์—์„œ ์กฐํšŒ) + properties: + storeId: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440002" + storeName: + type: string + example: "์šฐ์ง„๋„ค ๊ณ ๊นƒ์ง‘" + category: + type: string + example: "์Œ์‹์ " + description: + type: string + example: "์‹ ์„ ํ•œ ํ•œ์šฐ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ณ ๊นƒ์ง‘" + required: + - storeId + - storeName + - category + required: + - storeInfo + + JobAcceptedResponse: + type: object + properties: + jobId: + type: string + format: uuid + description: ์ƒ์„ฑ๋œ Job ID + example: "550e8400-e29b-41d4-a716-446655440005" + status: + type: string + enum: [PENDING] + description: Job ์ƒํƒœ (์ดˆ๊ธฐ ์ƒํƒœ๋Š” PENDING) + example: "PENDING" + message: + type: string + description: ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ + example: "AI ์ถ”์ฒœ ์ƒ์„ฑ ์š”์ฒญ์ด ์ ‘์ˆ˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. /jobs/{jobId}๋กœ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์„ธ์š”." + required: + - jobId + - status + - message + + JobStatusResponse: + type: object + properties: + jobId: + type: string + format: uuid + description: Job ID + example: "550e8400-e29b-41d4-a716-446655440005" + jobType: + type: string + enum: [AI_RECOMMENDATION, IMAGE_GENERATION] + description: Job ์œ ํ˜• + example: "AI_RECOMMENDATION" + status: + type: string + enum: [PENDING, PROCESSING, COMPLETED, FAILED] + description: Job ์ƒํƒœ + example: "COMPLETED" + progress: + type: integer + minimum: 0 + maximum: 100 + description: ์ง„ํ–‰๋ฅ  (%) + example: 100 + resultKey: + type: string + description: Redis ๊ฒฐ๊ณผ ํ‚ค (COMPLETED ์‹œ) + example: "ai:recommendation:550e8400-e29b-41d4-a716-446655440005" + errorMessage: + type: string + description: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ (FAILED ์‹œ) + example: "AI ์„œ๋น„์Šค ์—ฐ๊ฒฐ ์‹คํŒจ" + createdAt: + type: string + format: date-time + description: Job ์ƒ์„ฑ์ผ์‹œ + example: "2025-02-15T10:31:00Z" + completedAt: + type: string + format: date-time + description: Job ์™„๋ฃŒ์ผ์‹œ + example: "2025-02-15T10:31:30Z" + required: + - jobId + - jobType + - status + - progress + - createdAt + + SelectRecommendationRequest: + type: object + properties: + recommendationId: + type: string + format: uuid + description: ์„ ํƒํ•œ ์ถ”์ฒœ ID + example: "550e8400-e29b-41d4-a716-446655440007" + customizations: + type: object + description: ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ํ•ญ๋ชฉ + properties: + eventName: + type: string + description: ์ˆ˜์ •๋œ ์ด๋ฒคํŠธ๋ช… + example: "๋ด„๋งž์ด ํŠน๋ณ„ ํ• ์ธ ์ด๋ฒคํŠธ" + description: + type: string + description: ์ˆ˜์ •๋œ ์„ค๋ช… + example: "๋ด„์„ ๋งž์ดํ•˜์—ฌ ์ „ ๋ฉ”๋‰ด 20% ํ• ์ธ" + startDate: + type: string + format: date + description: ์ˆ˜์ •๋œ ์‹œ์ž‘์ผ + example: "2025-03-01" + endDate: + type: string + format: date + description: ์ˆ˜์ •๋œ ์ข…๋ฃŒ์ผ + example: "2025-03-31" + discountRate: + type: integer + description: ์ˆ˜์ •๋œ ํ• ์ธ์œจ + example: 20 + required: + - recommendationId + + ImageGenerationRequest: + type: object + properties: + eventInfo: + type: object + description: ์ด๋ฒคํŠธ ์ •๋ณด (์ด๋ฏธ์ง€ ์ƒ์„ฑ์— ํ•„์š”ํ•œ ์ •๋ณด) + properties: + eventName: + type: string + example: "๋ด„๋งž์ด 20% ํ• ์ธ ์ด๋ฒคํŠธ" + description: + type: string + example: "๋ด„์„ ๋งž์ดํ•˜์—ฌ ๋ชจ๋“  ์ƒํ’ˆ 20% ํ• ์ธ ํ–‰์‚ฌ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค." + promotionType: + type: string + example: "ํ• ์ธ" + required: + - eventName + - description + imageCount: + type: integer + minimum: 1 + maximum: 5 + description: ์ƒ์„ฑํ•  ์ด๋ฏธ์ง€ ๊ฐœ์ˆ˜ + default: 3 + example: 3 + required: + - eventInfo + + ImageEditRequest: + type: object + properties: + editType: + type: string + enum: [TEXT_OVERLAY, COLOR_ADJUST, CROP, FILTER] + description: ํŽธ์ง‘ ์œ ํ˜• + example: "TEXT_OVERLAY" + parameters: + type: object + description: ํŽธ์ง‘ ํŒŒ๋ผ๋ฏธํ„ฐ (ํŽธ์ง‘ ์œ ํ˜•์— ๋”ฐ๋ผ ๋‹ค๋ฆ„) + additionalProperties: true + example: + text: "20% ํ• ์ธ" + fontSize: 48 + color: "#FF0000" + position: "center" + required: + - editType + - parameters + + ImageEditResponse: + type: object + properties: + imageId: + type: string + format: uuid + description: ํŽธ์ง‘๋œ ์ด๋ฏธ์ง€ ID + example: "550e8400-e29b-41d4-a716-446655440008" + imageUrl: + type: string + format: uri + description: ํŽธ์ง‘๋œ ์ด๋ฏธ์ง€ URL + example: "https://cdn.kt-event.com/images/event-img-001-edited.jpg" + editedAt: + type: string + format: date-time + description: ํŽธ์ง‘์ผ์‹œ + example: "2025-02-16T15:20:00Z" + required: + - imageId + - imageUrl + - editedAt + + SelectChannelsRequest: + type: object + properties: + channels: + type: array + description: ๋ฐฐํฌ ์ฑ„๋„ ๋ชฉ๋ก + items: + type: string + enum: [WEBSITE, KAKAO, INSTAGRAM, FACEBOOK, NAVER_BLOG] + example: ["WEBSITE", "KAKAO", "INSTAGRAM"] + minItems: 1 + required: + - channels + + EventPublishedResponse: + type: object + properties: + eventId: + type: string + format: uuid + description: ์ด๋ฒคํŠธ ID + example: "550e8400-e29b-41d4-a716-446655440000" + status: + type: string + enum: [PUBLISHED] + description: ์ด๋ฒคํŠธ ์ƒํƒœ (ํ•ญ์ƒ PUBLISHED) + example: "PUBLISHED" + publishedAt: + type: string + format: date-time + description: ๋ฐฐํฌ์ผ์‹œ + example: "2025-02-20T16:00:00Z" + channels: + type: array + description: ๋ฐฐํฌ๋œ ์ฑ„๋„ ๋ชฉ๋ก + items: + type: string + example: "WEBSITE" + distributionResults: + type: array + description: ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ๊ฒฐ๊ณผ + items: + $ref: '#/components/schemas/DistributionResult' + required: + - eventId + - status + - publishedAt + - channels + - distributionResults + + DistributionResult: + type: object + properties: + channel: + type: string + description: ์ฑ„๋„๋ช… + example: "WEBSITE" + success: + type: boolean + description: ๋ฐฐํฌ ์„ฑ๊ณต ์—ฌ๋ถ€ + example: true + url: + type: string + format: uri + description: ๋ฐฐํฌ๋œ URL + example: "https://store.kt-event.com/event/550e8400-e29b-41d4-a716-446655440000" + message: + type: string + description: ๋ฐฐํฌ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + example: "์›น์‚ฌ์ดํŠธ์— ์„ฑ๊ณต์ ์œผ๋กœ ๋ฐฐํฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." + required: + - channel + - success + + UpdateEventRequest: + type: object + properties: + eventName: + type: string + description: ์ด๋ฒคํŠธ๋ช… + example: "๋ด„๋งž์ด ํŠน๋ณ„ ํ• ์ธ ์ด๋ฒคํŠธ" + description: + type: string + description: ์ด๋ฒคํŠธ ์„ค๋ช… + example: "๋ด„์„ ๋งž์ดํ•˜์—ฌ ์ „ ๋ฉ”๋‰ด 20% ํ• ์ธ" + startDate: + type: string + format: date + description: ์‹œ์ž‘์ผ + example: "2025-03-01" + endDate: + type: string + format: date + description: ์ข…๋ฃŒ์ผ + example: "2025-03-31" + discountRate: + type: integer + description: ํ• ์ธ์œจ + example: 20 + + EndEventRequest: + type: object + properties: + reason: + type: string + description: ์ข…๋ฃŒ ์‚ฌ์œ  + example: "๋ชฉํ‘œ ๋‹ฌ์„ฑ์œผ๋กœ ์กฐ๊ธฐ ์ข…๋ฃŒ" + required: + - reason + + PageInfo: + type: object + properties: + page: + type: integer + description: ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ + example: 0 + size: + type: integer + description: ํŽ˜์ด์ง€ ํฌ๊ธฐ + example: 20 + totalElements: + type: integer + description: ์ „์ฒด ์š”์†Œ ๊ฐœ์ˆ˜ + example: 45 + totalPages: + type: integer + description: ์ „์ฒด ํŽ˜์ด์ง€ ๊ฐœ์ˆ˜ + example: 3 + required: + - page + - size + - totalElements + - totalPages + + ErrorResponse: + type: object + required: + - code + - message + - timestamp + properties: + code: + type: string + description: ์—๋Ÿฌ ์ฝ”๋“œ + example: "INVALID_REQUEST" + message: + type: string + description: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + example: "์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค." + timestamp: + type: string + format: date-time + description: ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ๊ฐ + example: "2025-02-15T10:30:00Z" + details: + type: array + description: ์ƒ์„ธ ์—๋Ÿฌ ์ •๋ณด (์„ ํƒ ์‚ฌํ•ญ) + items: + type: string + example: ["objective ํ•„๋“œ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค."] + + responses: + BadRequest: + description: ์ž˜๋ชป๋œ ์š”์ฒญ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_REQUEST + message: ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + details: + - "objective ํ•„๋“œ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค." + timestamp: "2025-02-15T10:30:00Z" + + Unauthorized: + description: ์ธ์ฆ ์‹คํŒจ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: UNAUTHORIZED + message: ์ธ์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. + timestamp: "2025-02-15T10:30:00Z" + + Forbidden: + description: ๊ถŒํ•œ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: FORBIDDEN + message: ํ•ด๋‹น ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. + timestamp: "2025-02-15T10:30:00Z" + + NotFound: + description: ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: NOT_FOUND + message: ์š”์ฒญํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + timestamp: "2025-02-15T10:30:00Z" + + InternalServerError: + description: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INTERNAL_SERVER_ERROR + message: ์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. + timestamp: "2025-02-15T10:30:00Z" diff --git a/design/backend/api/participation-service-api.yaml b/design/backend/api/participation-service-api.yaml new file mode 100644 index 0000000..645da39 --- /dev/null +++ b/design/backend/api/participation-service-api.yaml @@ -0,0 +1,820 @@ +openapi: 3.0.3 +info: + title: Participation Service API + description: | + ์ด๋ฒคํŠธ ์ฐธ์—ฌ ๋ฐ ๋‹น์ฒจ์ž ๊ด€๋ฆฌ ์„œ๋น„์Šค API + - ์ด๋ฒคํŠธ ์ฐธ์—ฌ ๋“ฑ๋ก + - ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ ๋ฐ ๊ด€๋ฆฌ + - ๋‹น์ฒจ์ž ์ถ”์ฒจ ๋ฐ ๊ด€๋ฆฌ + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8084 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/participation/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/participation/v1 + description: Production Server + +tags: + - name: participation + description: ์ด๋ฒคํŠธ ์ฐธ์—ฌ ๊ด€๋ฆฌ + - name: participant + description: ์ฐธ์—ฌ์ž ์กฐํšŒ ๋ฐ ๊ด€๋ฆฌ + - name: winner + description: ๋‹น์ฒจ์ž ์ถ”์ฒจ ๋ฐ ๊ด€๋ฆฌ + +paths: + /events/{eventId}/participate: + post: + tags: + - participation + summary: ์ด๋ฒคํŠธ ์ฐธ์—ฌ + description: | + ๊ณ ๊ฐ์ด ์ด๋ฒคํŠธ์— ์ฐธ์—ฌํ•ฉ๋‹ˆ๋‹ค. + - ์ค‘๋ณต ์ฐธ์—ฌ ๊ฒ€์ฆ (์ „ํ™”๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜) + - ์ด๋ฒคํŠธ ์ง„ํ–‰ ์ƒํƒœ ๊ฒ€์ฆ + - Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ (ParticipantRegistered) + operationId: participateEvent + x-user-story: UFR-PART-010 + x-controller: ParticipationController + parameters: + - name: eventId + in: path + required: true + description: ์ด๋ฒคํŠธ ID + schema: + type: string + example: "evt_20250123_001" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipationRequest' + examples: + standard: + summary: ์ผ๋ฐ˜ ์ฐธ์—ฌ + value: + name: "ํ™๊ธธ๋™" + phoneNumber: "010-1234-5678" + email: "hong@example.com" + agreeMarketing: true + agreePrivacy: true + storeVisited: false + storeVisit: + summary: ๋งค์žฅ ๋ฐฉ๋ฌธ ์ฐธ์—ฌ + value: + name: "๊น€์ฒ ์ˆ˜" + phoneNumber: "010-9876-5432" + email: "kim@example.com" + agreeMarketing: false + agreePrivacy: true + storeVisited: true + responses: + '201': + description: ์ฐธ์—ฌ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipationResponse' + examples: + success: + summary: ์ฐธ์—ฌ ์„ฑ๊ณต + value: + success: true + message: "์ด๋ฒคํŠธ ์ฐธ์—ฌ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + data: + participantId: "prt_20250123_001" + eventId: "evt_20250123_001" + name: "ํ™๊ธธ๋™" + phoneNumber: "010-1234-5678" + email: "hong@example.com" + participatedAt: "2025-01-23T10:30:00Z" + storeVisited: false + bonusEntries: 1 + '400': + description: ์ž˜๋ชป๋œ ์š”์ฒญ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidPhone: + summary: ์œ ํšจํ•˜์ง€ ์•Š์€ ์ „ํ™”๋ฒˆํ˜ธ + value: + success: false + error: + code: "INVALID_PHONE_NUMBER" + message: "์œ ํšจํ•˜์ง€ ์•Š์€ ์ „ํ™”๋ฒˆํ˜ธ ํ˜•์‹์ž…๋‹ˆ๋‹ค" + duplicateParticipation: + summary: ์ค‘๋ณต ์ฐธ์—ฌ + value: + success: false + error: + code: "DUPLICATE_PARTICIPATION" + message: "์ด๋ฏธ ์ฐธ์—ฌํ•˜์‹  ์ด๋ฒคํŠธ์ž…๋‹ˆ๋‹ค" + '404': + description: ์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: ์ด๋ฒคํŠธ ์—†์Œ + value: + success: false + error: + code: "EVENT_NOT_FOUND" + message: "์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + '409': + description: ์ด๋ฒคํŠธ ์ง„ํ–‰ ๋ถˆ๊ฐ€ ์ƒํƒœ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notActive: + summary: ์ง„ํ–‰์ค‘์ด ์•„๋‹Œ ์ด๋ฒคํŠธ + value: + success: false + error: + code: "EVENT_NOT_ACTIVE" + message: "ํ˜„์žฌ ์ฐธ์—ฌํ•  ์ˆ˜ ์—†๋Š” ์ด๋ฒคํŠธ์ž…๋‹ˆ๋‹ค" + + /events/{eventId}/participants: + get: + tags: + - participant + summary: ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ + description: | + ์ด๋ฒคํŠธ์˜ ์ฐธ์—ฌ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + - ํŽ˜์ด์ง• ์ง€์› + - ์ฐธ์—ฌ์ผ์‹œ ๊ธฐ์ค€ ์ •๋ ฌ + - ๋งค์žฅ ๋ฐฉ๋ฌธ ์—ฌ๋ถ€ ํ•„ํ„ฐ๋ง + operationId: getParticipants + x-user-story: UFR-PART-020 + x-controller: ParticipantController + parameters: + - name: eventId + in: path + required: true + description: ์ด๋ฒคํŠธ ID + schema: + type: string + example: "evt_20250123_001" + - name: page + in: query + description: ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘) + schema: + type: integer + default: 0 + minimum: 0 + example: 0 + - name: size + in: query + description: ํŽ˜์ด์ง€ ํฌ๊ธฐ + schema: + type: integer + default: 20 + minimum: 1 + maximum: 100 + example: 20 + - name: storeVisited + in: query + description: ๋งค์žฅ ๋ฐฉ๋ฌธ ์—ฌ๋ถ€ ํ•„ํ„ฐ + schema: + type: boolean + example: true + responses: + '200': + description: ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantListResponse' + examples: + success: + summary: ์ฐธ์—ฌ์ž ๋ชฉ๋ก + value: + success: true + message: "์ฐธ์—ฌ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค" + data: + participants: + - participantId: "prt_20250123_001" + name: "ํ™๊ธธ๋™" + phoneNumber: "010-1234-5678" + email: "hong@example.com" + participatedAt: "2025-01-23T10:30:00Z" + storeVisited: false + bonusEntries: 1 + isWinner: false + - participantId: "prt_20250123_002" + name: "๊น€์ฒ ์ˆ˜" + phoneNumber: "010-9876-5432" + email: "kim@example.com" + participatedAt: "2025-01-23T11:15:00Z" + storeVisited: true + bonusEntries: 2 + isWinner: true + pagination: + currentPage: 0 + pageSize: 20 + totalElements: 156 + totalPages: 8 + hasNext: true + hasPrevious: false + '404': + description: ์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /events/{eventId}/participants/{participantId}: + get: + tags: + - participant + summary: ์ฐธ์—ฌ์ž ์ƒ์„ธ ์กฐํšŒ + description: ํŠน์ • ์ฐธ์—ฌ์ž์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + operationId: getParticipantDetail + x-user-story: UFR-PART-020 + x-controller: ParticipantController + parameters: + - name: eventId + in: path + required: true + description: ์ด๋ฒคํŠธ ID + schema: + type: string + example: "evt_20250123_001" + - name: participantId + in: path + required: true + description: ์ฐธ์—ฌ์ž ID + schema: + type: string + example: "prt_20250123_001" + responses: + '200': + description: ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantDetailResponse' + examples: + success: + summary: ์ฐธ์—ฌ์ž ์ƒ์„ธ ์ •๋ณด + value: + success: true + message: "์ฐธ์—ฌ์ž ์ •๋ณด๋ฅผ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค" + data: + participantId: "prt_20250123_001" + eventId: "evt_20250123_001" + name: "ํ™๊ธธ๋™" + phoneNumber: "010-1234-5678" + email: "hong@example.com" + participatedAt: "2025-01-23T10:30:00Z" + storeVisited: false + bonusEntries: 1 + agreeMarketing: true + agreePrivacy: true + isWinner: false + winnerInfo: null + '404': + description: ์ฐธ์—ฌ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: ์ฐธ์—ฌ์ž ์—†์Œ + value: + success: false + error: + code: "PARTICIPANT_NOT_FOUND" + message: "์ฐธ์—ฌ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + + /events/{eventId}/draw-winners: + post: + tags: + - winner + summary: ๋‹น์ฒจ์ž ์ถ”์ฒจ + description: | + ์ด๋ฒคํŠธ ๋‹น์ฒจ์ž๋ฅผ ์ถ”์ฒจํ•ฉ๋‹ˆ๋‹ค. + - ๋žœ๋ค ์ถ”์ฒจ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์‚ฌ์šฉ + - ๋งค์žฅ ๋ฐฉ๋ฌธ ๋ณด๋„ˆ์Šค ๊ฐ€์ค‘์น˜ ์ ์šฉ + - ์ค‘๋ณต ๋‹น์ฒจ ๋ฐฉ์ง€ + operationId: drawWinners + x-user-story: UFR-PART-030 + x-controller: WinnerController + parameters: + - name: eventId + in: path + required: true + description: ์ด๋ฒคํŠธ ID + schema: + type: string + example: "evt_20250123_001" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DrawWinnersRequest' + examples: + standard: + summary: ์ผ๋ฐ˜ ์ถ”์ฒจ + value: + winnerCount: 10 + applyStoreVisitBonus: true + responses: + '200': + description: ์ถ”์ฒจ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/DrawWinnersResponse' + examples: + success: + summary: ์ถ”์ฒจ ์™„๋ฃŒ + value: + success: true + message: "๋‹น์ฒจ์ž ์ถ”์ฒจ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + data: + eventId: "evt_20250123_001" + totalParticipants: 156 + winnerCount: 10 + drawnAt: "2025-01-24T15:00:00Z" + winners: + - participantId: "prt_20250123_002" + name: "๊น€์ฒ ์ˆ˜" + phoneNumber: "010-9876-5432" + rank: 1 + - participantId: "prt_20250123_045" + name: "์ด์˜ํฌ" + phoneNumber: "010-5555-1234" + rank: 2 + - participantId: "prt_20250123_089" + name: "๋ฐ•๋ฏผ์ˆ˜" + phoneNumber: "010-7777-8888" + rank: 3 + '400': + description: ์ž˜๋ชป๋œ ์š”์ฒญ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidCount: + summary: ์ž˜๋ชป๋œ ๋‹น์ฒจ์ž ์ˆ˜ + value: + success: false + error: + code: "INVALID_WINNER_COUNT" + message: "๋‹น์ฒจ์ž ์ˆ˜๊ฐ€ ์ฐธ์—ฌ์ž ์ˆ˜๋ณด๋‹ค ๋งŽ์Šต๋‹ˆ๋‹ค" + '404': + description: ์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: ์ด๋ฏธ ์ถ”์ฒจ ์™„๋ฃŒ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + alreadyDrawn: + summary: ์ถ”์ฒจ ์™„๋ฃŒ ์ƒํƒœ + value: + success: false + error: + code: "ALREADY_DRAWN" + message: "์ด๋ฏธ ๋‹น์ฒจ์ž ์ถ”์ฒจ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + + /events/{eventId}/winners: + get: + tags: + - winner + summary: ๋‹น์ฒจ์ž ๋ชฉ๋ก ์กฐํšŒ + description: | + ์ด๋ฒคํŠธ์˜ ๋‹น์ฒจ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + - ๋‹น์ฒจ ์ˆœ์œ„๋ณ„ ์ •๋ ฌ + - ํŽ˜์ด์ง• ์ง€์› + operationId: getWinners + x-user-story: UFR-PART-030 + x-controller: WinnerController + parameters: + - name: eventId + in: path + required: true + description: ์ด๋ฒคํŠธ ID + schema: + type: string + example: "evt_20250123_001" + - name: page + in: query + description: ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘) + schema: + type: integer + default: 0 + minimum: 0 + example: 0 + - name: size + in: query + description: ํŽ˜์ด์ง€ ํฌ๊ธฐ + schema: + type: integer + default: 20 + minimum: 1 + maximum: 100 + example: 20 + responses: + '200': + description: ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/WinnerListResponse' + examples: + success: + summary: ๋‹น์ฒจ์ž ๋ชฉ๋ก + value: + success: true + message: "๋‹น์ฒจ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค" + data: + eventId: "evt_20250123_001" + drawnAt: "2025-01-24T15:00:00Z" + totalWinners: 10 + winners: + - participantId: "prt_20250123_002" + name: "๊น€์ฒ ์ˆ˜" + phoneNumber: "010-9876-5432" + email: "kim@example.com" + rank: 1 + wonAt: "2025-01-24T15:00:00Z" + - participantId: "prt_20250123_045" + name: "์ด์˜ํฌ" + phoneNumber: "010-5555-1234" + email: "lee@example.com" + rank: 2 + wonAt: "2025-01-24T15:00:00Z" + pagination: + currentPage: 0 + pageSize: 20 + totalElements: 10 + totalPages: 1 + hasNext: false + hasPrevious: false + '404': + description: ์ด๋ฒคํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ ๋˜๋Š” ๋‹น์ฒจ์ž๊ฐ€ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + noWinners: + summary: ๋‹น์ฒจ์ž ์—†์Œ + value: + success: false + error: + code: "NO_WINNERS_YET" + message: "์•„์ง ๋‹น์ฒจ์ž ์ถ”์ฒจ์ด ์ง„ํ–‰๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค" + +components: + schemas: + ParticipationRequest: + type: object + required: + - name + - phoneNumber + - agreePrivacy + properties: + name: + type: string + description: ์ฐธ์—ฌ์ž ์ด๋ฆ„ + minLength: 2 + maxLength: 50 + example: "ํ™๊ธธ๋™" + phoneNumber: + type: string + description: ์ฐธ์—ฌ์ž ์ „ํ™”๋ฒˆํ˜ธ (ํ•˜์ดํ”ˆ ํฌํ•จ) + pattern: '^\d{3}-\d{3,4}-\d{4}$' + example: "010-1234-5678" + email: + type: string + format: email + description: ์ฐธ์—ฌ์ž ์ด๋ฉ”์ผ + example: "hong@example.com" + agreeMarketing: + type: boolean + description: ๋งˆ์ผ€ํŒ… ์ •๋ณด ์ˆ˜์‹  ๋™์˜ + default: false + example: true + agreePrivacy: + type: boolean + description: ๊ฐœ์ธ์ •๋ณด ์ˆ˜์ง‘ ๋ฐ ์ด์šฉ ๋™์˜ (ํ•„์ˆ˜) + example: true + storeVisited: + type: boolean + description: ๋งค์žฅ ๋ฐฉ๋ฌธ ์—ฌ๋ถ€ + default: false + example: false + + ParticipationResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "์ด๋ฒคํŠธ ์ฐธ์—ฌ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + data: + $ref: '#/components/schemas/ParticipantInfo' + + ParticipantInfo: + type: object + properties: + participantId: + type: string + description: ์ฐธ์—ฌ์ž ID + example: "prt_20250123_001" + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt_20250123_001" + name: + type: string + description: ์ฐธ์—ฌ์ž ์ด๋ฆ„ + example: "ํ™๊ธธ๋™" + phoneNumber: + type: string + description: ์ฐธ์—ฌ์ž ์ „ํ™”๋ฒˆํ˜ธ + example: "010-1234-5678" + email: + type: string + description: ์ฐธ์—ฌ์ž ์ด๋ฉ”์ผ + example: "hong@example.com" + participatedAt: + type: string + format: date-time + description: ์ฐธ์—ฌ ์ผ์‹œ + example: "2025-01-23T10:30:00Z" + storeVisited: + type: boolean + description: ๋งค์žฅ ๋ฐฉ๋ฌธ ์—ฌ๋ถ€ + example: false + bonusEntries: + type: integer + description: ๋ณด๋„ˆ์Šค ์‘๋ชจ๊ถŒ ์ˆ˜ (๋งค์žฅ ๋ฐฉ๋ฌธ ์‹œ +1) + minimum: 1 + example: 1 + isWinner: + type: boolean + description: ๋‹น์ฒจ ์—ฌ๋ถ€ + example: false + + ParticipantDetailInfo: + allOf: + - $ref: '#/components/schemas/ParticipantInfo' + - type: object + properties: + agreeMarketing: + type: boolean + description: ๋งˆ์ผ€ํŒ… ์ •๋ณด ์ˆ˜์‹  ๋™์˜ + example: true + agreePrivacy: + type: boolean + description: ๊ฐœ์ธ์ •๋ณด ์ˆ˜์ง‘ ๋ฐ ์ด์šฉ ๋™์˜ + example: true + winnerInfo: + $ref: '#/components/schemas/WinnerInfo' + nullable: true + + ParticipantListResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "์ฐธ์—ฌ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค" + data: + type: object + properties: + participants: + type: array + items: + $ref: '#/components/schemas/ParticipantInfo' + pagination: + $ref: '#/components/schemas/Pagination' + + ParticipantDetailResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "์ฐธ์—ฌ์ž ์ •๋ณด๋ฅผ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค" + data: + $ref: '#/components/schemas/ParticipantDetailInfo' + + DrawWinnersRequest: + type: object + required: + - winnerCount + properties: + winnerCount: + type: integer + description: ๋‹น์ฒจ์ž ์ˆ˜ + minimum: 1 + example: 10 + applyStoreVisitBonus: + type: boolean + description: ๋งค์žฅ ๋ฐฉ๋ฌธ ๋ณด๋„ˆ์Šค ์ ์šฉ ์—ฌ๋ถ€ + default: true + example: true + + DrawWinnersResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "๋‹น์ฒจ์ž ์ถ”์ฒจ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + data: + type: object + properties: + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt_20250123_001" + totalParticipants: + type: integer + description: ์ „์ฒด ์ฐธ์—ฌ์ž ์ˆ˜ + example: 156 + winnerCount: + type: integer + description: ๋‹น์ฒจ์ž ์ˆ˜ + example: 10 + drawnAt: + type: string + format: date-time + description: ์ถ”์ฒจ ์ผ์‹œ + example: "2025-01-24T15:00:00Z" + winners: + type: array + description: ๋‹น์ฒจ์ž ๋ชฉ๋ก + items: + $ref: '#/components/schemas/WinnerSummary' + + WinnerSummary: + type: object + properties: + participantId: + type: string + description: ์ฐธ์—ฌ์ž ID + example: "prt_20250123_002" + name: + type: string + description: ๋‹น์ฒจ์ž ์ด๋ฆ„ + example: "๊น€์ฒ ์ˆ˜" + phoneNumber: + type: string + description: ๋‹น์ฒจ์ž ์ „ํ™”๋ฒˆํ˜ธ + example: "010-9876-5432" + rank: + type: integer + description: ๋‹น์ฒจ ์ˆœ์œ„ + minimum: 1 + example: 1 + + WinnerInfo: + type: object + properties: + participantId: + type: string + description: ์ฐธ์—ฌ์ž ID + example: "prt_20250123_002" + name: + type: string + description: ๋‹น์ฒจ์ž ์ด๋ฆ„ + example: "๊น€์ฒ ์ˆ˜" + phoneNumber: + type: string + description: ๋‹น์ฒจ์ž ์ „ํ™”๋ฒˆํ˜ธ + example: "010-9876-5432" + email: + type: string + description: ๋‹น์ฒจ์ž ์ด๋ฉ”์ผ + example: "kim@example.com" + rank: + type: integer + description: ๋‹น์ฒจ ์ˆœ์œ„ + minimum: 1 + example: 1 + wonAt: + type: string + format: date-time + description: ๋‹น์ฒจ ์ผ์‹œ + example: "2025-01-24T15:00:00Z" + + WinnerListResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "๋‹น์ฒจ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ–ˆ์Šต๋‹ˆ๋‹ค" + data: + type: object + properties: + eventId: + type: string + description: ์ด๋ฒคํŠธ ID + example: "evt_20250123_001" + drawnAt: + type: string + format: date-time + description: ์ถ”์ฒจ ์ผ์‹œ + example: "2025-01-24T15:00:00Z" + totalWinners: + type: integer + description: ์ „์ฒด ๋‹น์ฒจ์ž ์ˆ˜ + example: 10 + winners: + type: array + items: + $ref: '#/components/schemas/WinnerInfo' + pagination: + $ref: '#/components/schemas/Pagination' + + Pagination: + type: object + properties: + currentPage: + type: integer + description: ํ˜„์žฌ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘) + minimum: 0 + example: 0 + pageSize: + type: integer + description: ํŽ˜์ด์ง€ ํฌ๊ธฐ + minimum: 1 + example: 20 + totalElements: + type: integer + description: ์ „์ฒด ์š”์†Œ ์ˆ˜ + minimum: 0 + example: 156 + totalPages: + type: integer + description: ์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜ + minimum: 0 + example: 8 + hasNext: + type: boolean + description: ๋‹ค์Œ ํŽ˜์ด์ง€ ์กด์žฌ ์—ฌ๋ถ€ + example: true + hasPrevious: + type: boolean + description: ์ด์ „ ํŽ˜์ด์ง€ ์กด์žฌ ์—ฌ๋ถ€ + example: false + + ErrorResponse: + type: object + properties: + success: + type: boolean + example: false + error: + type: object + properties: + code: + type: string + description: ์—๋Ÿฌ ์ฝ”๋“œ + example: "DUPLICATE_PARTICIPATION" + message: + type: string + description: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + example: "์ด๋ฏธ ์ฐธ์—ฌํ•˜์‹  ์ด๋ฒคํŠธ์ž…๋‹ˆ๋‹ค" + details: + type: object + description: ์ถ”๊ฐ€ ์—๋Ÿฌ ์ƒ์„ธ ์ •๋ณด + additionalProperties: true + nullable: true diff --git a/design/backend/api/user-service-api.yaml b/design/backend/api/user-service-api.yaml new file mode 100644 index 0000000..e1c486f --- /dev/null +++ b/design/backend/api/user-service-api.yaml @@ -0,0 +1,991 @@ +openapi: 3.0.3 +info: + title: User Service API + description: | + KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - User Service API + + ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๋งค์žฅ์ •๋ณด ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค + + **์ฃผ์š” ๊ธฐ๋Šฅ:** + - ํšŒ์›๊ฐ€์ž… + - ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ + - ํ”„๋กœํ•„ ์กฐํšŒ ๋ฐ ์ˆ˜์ • + - ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ + + **๋ณด์•ˆ:** + - JWT Bearer ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ + - bcrypt ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8081 + description: Local Development Server + - url: https://dev-api.kt-event-marketing.com/user/v1 + description: Development Server + - url: https://api.kt-event-marketing.com/user/v1 + description: Production Server + +tags: + - name: Authentication + description: ์ธ์ฆ ๊ด€๋ จ API (๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ, ํšŒ์›๊ฐ€์ž…) + - name: Profile + description: ํ”„๋กœํ•„ ๊ด€๋ จ API (์กฐํšŒ, ์ˆ˜์ •, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ) + +paths: + /users/register: + post: + tags: + - Authentication + summary: ํšŒ์›๊ฐ€์ž… + description: | + ์†Œ์ƒ๊ณต์ธ ํšŒ์›๊ฐ€์ž… API + + **์œ ์ €์Šคํ† ๋ฆฌ:** UFR-USER-010 + + **์ฃผ์š” ๊ธฐ๋Šฅ:** + - ๊ธฐ๋ณธ ์ •๋ณด ๋ฐ ๋งค์žฅ ์ •๋ณด ๋“ฑ๋ก + - ๋น„๋ฐ€๋ฒˆํ˜ธ bcrypt ํ•ด์‹ฑ + - JWT ํ† ํฐ ์ž๋™ ๋ฐœ๊ธ‰ + + **์ฒ˜๋ฆฌ ํ๋ฆ„:** + 1. ์ค‘๋ณต ์‚ฌ์šฉ์ž ํ™•์ธ (์ „ํ™”๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜) + 2. ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ (bcrypt) + 3. User/Store ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ + 4. JWT ํ† ํฐ ์ƒ์„ฑ ๋ฐ ์„ธ์…˜ ์ €์žฅ (Redis) + operationId: registerUser + x-user-story: UFR-USER-010 + x-controller: UserController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + examples: + restaurant: + summary: ์Œ์‹์  ํšŒ์›๊ฐ€์ž… ์˜ˆ์‹œ + value: + name: ํ™๊ธธ๋™ + phoneNumber: "01012345678" + email: hong@example.com + password: "Password123!" + storeName: ๋ง›์žˆ๋Š”์ง‘ + industry: ์Œ์‹์  + address: ์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123 + businessHours: "์›”-๊ธˆ 11:00-22:00, ํ† -์ผ 12:00-21:00" + cafe: + summary: ์นดํŽ˜ ํšŒ์›๊ฐ€์ž… ์˜ˆ์‹œ + value: + name: ๊น€์ฒ ์ˆ˜ + phoneNumber: "01087654321" + email: kim@example.com + password: "SecurePass456!" + storeName: ์•„๋ฉ”๋ฆฌ์นด๋…ธ ์นดํŽ˜ + industry: ์นดํŽ˜ + address: ์„œ์šธ์‹œ ์„œ์ดˆ๊ตฌ ์„œ์ดˆ๋Œ€๋กœ 456 + businessHours: "๋งค์ผ 09:00-20:00" + responses: + '201': + description: ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterResponse' + examples: + success: + summary: ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต ์‘๋‹ต + value: + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: 123 + userName: ํ™๊ธธ๋™ + storeId: 456 + storeName: ๋ง›์žˆ๋Š”์ง‘ + '400': + description: ์ž˜๋ชป๋œ ์š”์ฒญ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + duplicateUser: + summary: ์ค‘๋ณต ์‚ฌ์šฉ์ž + value: + code: USER_001 + message: ์ด๋ฏธ ๊ฐ€์ž…๋œ ์ „ํ™”๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + validationError: + summary: ์ž…๋ ฅ ๊ฒ€์ฆ ์˜ค๋ฅ˜ + value: + code: VALIDATION_ERROR + message: ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + '500': + description: ์„œ๋ฒ„ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /users/login: + post: + tags: + - Authentication + summary: ๋กœ๊ทธ์ธ + description: | + ์†Œ์ƒ๊ณต์ธ ๋กœ๊ทธ์ธ API + + **์œ ์ €์Šคํ† ๋ฆฌ:** UFR-USER-020 + + **์ฃผ์š” ๊ธฐ๋Šฅ:** + - ์ „ํ™”๋ฒˆํ˜ธ/๋น„๋ฐ€๋ฒˆํ˜ธ ์ธ์ฆ + - JWT ํ† ํฐ ๋ฐœ๊ธ‰ + - Redis ์„ธ์…˜ ์ €์žฅ + - ์ตœ์ข… ๋กœ๊ทธ์ธ ์‹œ๊ฐ ์—…๋ฐ์ดํŠธ (๋น„๋™๊ธฐ) + + **๋ณด์•ˆ:** + - Timing Attack ๋ฐฉ์–ด (์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ†ต์ผ) + - bcrypt ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ + - JWT ํ† ํฐ 7์ผ ๋งŒ๋ฃŒ + operationId: loginUser + x-user-story: UFR-USER-020 + x-controller: UserController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + examples: + default: + summary: ๋กœ๊ทธ์ธ ์š”์ฒญ ์˜ˆ์‹œ + value: + phoneNumber: "01012345678" + password: "Password123!" + responses: + '200': + description: ๋กœ๊ทธ์ธ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + examples: + success: + summary: ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‘๋‹ต + value: + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: 123 + userName: ํ™๊ธธ๋™ + role: OWNER + email: hong@example.com + '401': + description: ์ธ์ฆ ์‹คํŒจ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + authFailed: + summary: ์ธ์ฆ ์‹คํŒจ + value: + code: AUTH_001 + message: ์ „ํ™”๋ฒˆํ˜ธ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š” + timestamp: 2025-10-22T10:30:00Z + + /users/logout: + post: + tags: + - Authentication + summary: ๋กœ๊ทธ์•„์›ƒ + description: | + ๋กœ๊ทธ์•„์›ƒ API + + **์œ ์ €์Šคํ† ๋ฆฌ:** UFR-USER-040 + + **์ฃผ์š” ๊ธฐ๋Šฅ:** + - Redis ์„ธ์…˜ ์‚ญ์ œ + - JWT ํ† ํฐ Blacklist ์ถ”๊ฐ€ + - ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ + + **์ฒ˜๋ฆฌ ํ๋ฆ„:** + 1. JWT ํ† ํฐ ๊ฒ€์ฆ + 2. Redis ์„ธ์…˜ ์‚ญ์ œ + 3. JWT Blacklist ์ถ”๊ฐ€ (๋‚จ์€ ๋งŒ๋ฃŒ ์‹œ๊ฐ„๋งŒํผ TTL ์„ค์ •) + 4. ๋กœ๊ทธ์•„์›ƒ ์ด๋ฒคํŠธ ๋ฐœํ–‰ + operationId: logoutUser + x-user-story: UFR-USER-040 + x-controller: UserController + security: + - BearerAuth: [] + responses: + '200': + description: ๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/LogoutResponse' + examples: + success: + summary: ๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต ์‘๋‹ต + value: + success: true + message: ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค + '401': + description: ์ธ์ฆ ์‹คํŒจ (์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidToken: + summary: ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ + value: + code: AUTH_002 + message: ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + + /users/profile: + get: + tags: + - Profile + summary: ํ”„๋กœํ•„ ์กฐํšŒ + description: | + ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์กฐํšŒ API + + **์œ ์ €์Šคํ† ๋ฆฌ:** UFR-USER-030 + + **์กฐํšŒ ์ •๋ณด:** + - ๊ธฐ๋ณธ ์ •๋ณด (์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ) + - ๋งค์žฅ ์ •๋ณด (๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ, ์˜์—…์‹œ๊ฐ„) + operationId: getProfile + x-user-story: UFR-USER-030 + x-controller: UserController + security: + - BearerAuth: [] + responses: + '200': + description: ํ”„๋กœํ•„ ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileResponse' + examples: + success: + summary: ํ”„๋กœํ•„ ์กฐํšŒ ์„ฑ๊ณต ์‘๋‹ต + value: + userId: 123 + userName: ํ™๊ธธ๋™ + phoneNumber: "01012345678" + email: hong@example.com + role: OWNER + storeId: 456 + storeName: ๋ง›์žˆ๋Š”์ง‘ + industry: ์Œ์‹์  + address: ์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123 + businessHours: "์›”-๊ธˆ 11:00-22:00, ํ† -์ผ 12:00-21:00" + createdAt: 2025-09-01T10:00:00Z + lastLoginAt: 2025-10-22T09:00:00Z + '401': + description: ์ธ์ฆ ์‹คํŒจ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: ์‚ฌ์šฉ์ž ์—†์Œ + value: + code: USER_003 + message: ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + + put: + tags: + - Profile + summary: ํ”„๋กœํ•„ ์ˆ˜์ • + description: | + ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ˆ˜์ • API + + **์œ ์ €์Šคํ† ๋ฆฌ:** UFR-USER-030 + + **์ˆ˜์ • ๊ฐ€๋Šฅ ํ•ญ๋ชฉ:** + - ๊ธฐ๋ณธ ์ •๋ณด: ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ + - ๋งค์žฅ ์ •๋ณด: ๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ, ์˜์—…์‹œ๊ฐ„ + + **์ฃผ์˜์‚ฌํ•ญ:** + - ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ์€ ๋ณ„๋„ API ์‚ฌ์šฉ (/users/password) + - ์ „ํ™”๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์‹œ ํ–ฅํ›„ ์žฌ์ธ์ฆ ํ•„์š” (ํ˜„์žฌ๋Š” ์ง์ ‘ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ) + - Optimistic Locking์œผ๋กœ ๋™์‹œ์„ฑ ์ œ์–ด + operationId: updateProfile + x-user-story: UFR-USER-030 + x-controller: UserController + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProfileRequest' + examples: + fullUpdate: + summary: ์ „์ฒด ์ •๋ณด ์ˆ˜์ • + value: + name: ํ™๊ธธ๋™ + phoneNumber: "01012345678" + email: hong.new@example.com + storeName: ๋ง›์žˆ๋Š”์ง‘ (๋ฆฌ๋‰ด์–ผ) + industry: ํ“จ์ „์Œ์‹์  + address: ์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 456 + businessHours: "๋งค์ผ 11:00-23:00" + partialUpdate: + summary: ์ผ๋ถ€ ์ •๋ณด ์ˆ˜์ • (์ด๋ฉ”์ผ, ์˜์—…์‹œ๊ฐ„) + value: + email: hong.updated@example.com + businessHours: "์›”-๊ธˆ 10:00-22:00, ํ† -์ผ ํœด๋ฌด" + responses: + '200': + description: ํ”„๋กœํ•„ ์ˆ˜์ • ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProfileResponse' + examples: + success: + summary: ํ”„๋กœํ•„ ์ˆ˜์ • ์„ฑ๊ณต ์‘๋‹ต + value: + userId: 123 + userName: ํ™๊ธธ๋™ + email: hong.new@example.com + storeId: 456 + storeName: ๋ง›์žˆ๋Š”์ง‘ (๋ฆฌ๋‰ด์–ผ) + '400': + description: ์ž˜๋ชป๋œ ์š”์ฒญ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: ์ธ์ฆ ์‹คํŒจ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: ์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: ๋™์‹œ์„ฑ ์ถฉ๋Œ (๋‹ค๋ฅธ ์„ธ์…˜์—์„œ ์ˆ˜์ •) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + conflict: + summary: ๋™์‹œ์„ฑ ์ถฉ๋Œ + value: + code: USER_005 + message: ๋‹ค๋ฅธ ์„ธ์…˜์—์„œ ํ”„๋กœํ•„์„ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ๊ณ ์นจ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š” + timestamp: 2025-10-22T10:30:00Z + + /users/password: + put: + tags: + - Profile + summary: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ + description: | + ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ API + + **์œ ์ €์Šคํ† ๋ฆฌ:** UFR-USER-030 + + **์ฃผ์š” ๊ธฐ๋Šฅ:** + - ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ํ•„์ˆ˜ + - ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ทœ์น™ ๊ฒ€์ฆ (8์ž ์ด์ƒ, ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ) + - bcrypt ํ•ด์‹ฑ + + **๋ณด์•ˆ:** + - ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ ์‹คํŒจ ์‹œ 400 Bad Request + - ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ํ›„ ๊ธฐ์กด ์„ธ์…˜ ์œ ์ง€ (๋กœ๊ทธ์•„์›ƒ ๋ถˆํ•„์š”) + operationId: changePassword + x-user-story: UFR-USER-030 + x-controller: UserController + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChangePasswordRequest' + examples: + default: + summary: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์š”์ฒญ + value: + currentPassword: "Password123!" + newPassword: "NewSecurePass456!" + responses: + '200': + description: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/ChangePasswordResponse' + examples: + success: + summary: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์„ฑ๊ณต ์‘๋‹ต + value: + success: true + message: ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค + '400': + description: ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ ๋˜๋Š” ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ทœ์น™ ์œ„๋ฐ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidCurrentPassword: + summary: ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ + value: + code: USER_004 + message: ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + invalidNewPassword: + summary: ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ทœ์น™ ์œ„๋ฐ˜ + value: + code: VALIDATION_ERROR + message: ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•˜๋ฉฐ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + '401': + description: ์ธ์ฆ ์‹คํŒจ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /users/{userId}/store: + get: + tags: + - Profile + summary: ๋งค์žฅ์ •๋ณด ์กฐํšŒ (์„œ๋น„์Šค ์—ฐ๋™์šฉ) + description: | + ํŠน์ • ์‚ฌ์šฉ์ž์˜ ๋งค์žฅ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” API (๋‚ด๋ถ€ ์„œ๋น„์Šค ์—ฐ๋™์šฉ) + + **์‚ฌ์šฉ ๋ชฉ์ :** + - Event Service์—์„œ ์ด๋ฒคํŠธ ์ƒ์„ฑ ์‹œ ๋งค์žฅ์ •๋ณด ์กฐํšŒ + - Content Service์—์„œ ๋งค์žฅ์ •๋ณด ๊ธฐ๋ฐ˜ ์ฝ˜ํ…์ธ  ์ƒ์„ฑ + - Service-to-Service ํ†ต์‹ ์šฉ ๋‚ด๋ถ€ API + + **์ฃผ์˜์‚ฌํ•ญ:** + - Internal API๋กœ ์™ธ๋ถ€ ๋…ธ์ถœ ๊ธˆ์ง€ + - API Gateway์—์„œ ์ธ์ฆ๋œ ์„œ๋น„์Šค๋งŒ ์ ‘๊ทผ ํ—ˆ์šฉ + - ๋งค์žฅ์ •๋ณด๋Š” Redis ์บ์‹œ ์šฐ์„  ์กฐํšŒ (TTL 30๋ถ„) + operationId: getStoreByUserId + x-user-story: Service Integration + x-controller: UserController + security: + - BearerAuth: [] + parameters: + - name: userId + in: path + required: true + description: ์‚ฌ์šฉ์ž ID + schema: + type: integer + format: int64 + example: 123 + responses: + '200': + description: ๋งค์žฅ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต + content: + application/json: + schema: + $ref: '#/components/schemas/StoreDetailResponse' + examples: + success: + summary: ๋งค์žฅ์ •๋ณด ์กฐํšŒ ์„ฑ๊ณต ์‘๋‹ต + value: + userId: 123 + storeId: 456 + storeName: ๋ง›์žˆ๋Š”์ง‘ + industry: ์Œ์‹์  + address: ์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123 + businessHours: "์›”-๊ธˆ 11:00-22:00, ํ† -์ผ 12:00-21:00" + '401': + description: ์ธ์ฆ ์‹คํŒจ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + unauthorized: + summary: ์ธ์ฆ ์‹คํŒจ + value: + code: AUTH_002 + message: ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + '403': + description: ๊ถŒํ•œ ์—†์Œ (๋‚ด๋ถ€ ์„œ๋น„์Šค๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + forbidden: + summary: ๊ถŒํ•œ ์—†์Œ + value: + code: AUTH_003 + message: ์ด API๋Š” ๋‚ด๋ถ€ ์„œ๋น„์Šค๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + '404': + description: ์‚ฌ์šฉ์ž ๋˜๋Š” ๋งค์žฅ์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: ์‚ฌ์šฉ์ž ๋˜๋Š” ๋งค์žฅ ์—†์Œ + value: + code: USER_003 + message: ์‚ฌ์šฉ์ž ๋˜๋Š” ๋งค์žฅ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค + timestamp: 2025-10-22T10:30:00Z + '500': + description: ์„œ๋ฒ„ ์˜ค๋ฅ˜ + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT Bearer ํ† ํฐ ์ธ์ฆ + + **ํ˜•์‹:** Authorization: Bearer {JWT_TOKEN} + + **ํ† ํฐ ๋งŒ๋ฃŒ:** 7์ผ + + **Claims:** + - userId: ์‚ฌ์šฉ์ž ID + - role: ์‚ฌ์šฉ์ž ์—ญํ•  (OWNER) + - iat: ๋ฐœ๊ธ‰ ์‹œ๊ฐ + - exp: ๋งŒ๋ฃŒ ์‹œ๊ฐ + + schemas: + RegisterRequest: + type: object + required: + - name + - phoneNumber + - email + - password + - storeName + - industry + - address + properties: + name: + type: string + minLength: 2 + maxLength: 50 + description: ์‚ฌ์šฉ์ž ์ด๋ฆ„ (2์ž ์ด์ƒ, ํ•œ๊ธ€/์˜๋ฌธ) + example: ํ™๊ธธ๋™ + phoneNumber: + type: string + pattern: '^010\d{8}$' + description: ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ (010XXXXXXXX) + example: "01012345678" + email: + type: string + format: email + maxLength: 100 + description: ์ด๋ฉ”์ผ ์ฃผ์†Œ + example: hong@example.com + password: + type: string + minLength: 8 + maxLength: 100 + description: ๋น„๋ฐ€๋ฒˆํ˜ธ (8์ž ์ด์ƒ, ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ) + example: "Password123!" + storeName: + type: string + minLength: 2 + maxLength: 100 + description: ๋งค์žฅ๋ช… + example: ๋ง›์žˆ๋Š”์ง‘ + industry: + type: string + maxLength: 50 + description: ์—…์ข… (์˜ˆ ์Œ์‹์ , ์นดํŽ˜, ์†Œ๋งค์  ๋“ฑ) + example: ์Œ์‹์  + address: + type: string + minLength: 5 + maxLength: 200 + description: ๋งค์žฅ ์ฃผ์†Œ + example: ์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123 + businessHours: + type: string + maxLength: 200 + description: ์˜์—…์‹œ๊ฐ„ (์„ ํƒ ์‚ฌํ•ญ) + example: "์›”-๊ธˆ 11:00-22:00, ํ† -์ผ 12:00-21:00" + + RegisterResponse: + type: object + required: + - token + - userId + - userName + - storeId + - storeName + properties: + token: + type: string + description: JWT ํ† ํฐ (7์ผ ๋งŒ๋ฃŒ) + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: + type: integer + format: int64 + description: ์‚ฌ์šฉ์ž ID + example: 123 + userName: + type: string + description: ์‚ฌ์šฉ์ž ์ด๋ฆ„ + example: ํ™๊ธธ๋™ + storeId: + type: integer + format: int64 + description: ๋งค์žฅ ID + example: 456 + storeName: + type: string + description: ๋งค์žฅ๋ช… + example: ๋ง›์žˆ๋Š”์ง‘ + + LoginRequest: + type: object + required: + - phoneNumber + - password + properties: + phoneNumber: + type: string + pattern: '^010\d{8}$' + description: ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ + example: "01012345678" + password: + type: string + minLength: 8 + description: ๋น„๋ฐ€๋ฒˆํ˜ธ + example: "Password123!" + + LoginResponse: + type: object + required: + - token + - userId + - userName + - role + - email + properties: + token: + type: string + description: JWT ํ† ํฐ (7์ผ ๋งŒ๋ฃŒ) + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: + type: integer + format: int64 + description: ์‚ฌ์šฉ์ž ID + example: 123 + userName: + type: string + description: ์‚ฌ์šฉ์ž ์ด๋ฆ„ + example: ํ™๊ธธ๋™ + role: + type: string + enum: [OWNER, ADMIN] + description: ์‚ฌ์šฉ์ž ์—ญํ•  + example: OWNER + email: + type: string + format: email + description: ์ด๋ฉ”์ผ ์ฃผ์†Œ + example: hong@example.com + + LogoutResponse: + type: object + required: + - success + - message + properties: + success: + type: boolean + description: ๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต ์—ฌ๋ถ€ + example: true + message: + type: string + description: ์‘๋‹ต ๋ฉ”์‹œ์ง€ + example: ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค + + ProfileResponse: + type: object + required: + - userId + - userName + - phoneNumber + - email + - role + - storeId + - storeName + - industry + - address + properties: + userId: + type: integer + format: int64 + description: ์‚ฌ์šฉ์ž ID + example: 123 + userName: + type: string + description: ์‚ฌ์šฉ์ž ์ด๋ฆ„ + example: ํ™๊ธธ๋™ + phoneNumber: + type: string + description: ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ + example: "01012345678" + email: + type: string + format: email + description: ์ด๋ฉ”์ผ ์ฃผ์†Œ + example: hong@example.com + role: + type: string + enum: [OWNER, ADMIN] + description: ์‚ฌ์šฉ์ž ์—ญํ•  + example: OWNER + storeId: + type: integer + format: int64 + description: ๋งค์žฅ ID + example: 456 + storeName: + type: string + description: ๋งค์žฅ๋ช… + example: ๋ง›์žˆ๋Š”์ง‘ + industry: + type: string + description: ์—…์ข… + example: ์Œ์‹์  + address: + type: string + description: ๋งค์žฅ ์ฃผ์†Œ + example: ์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123 + businessHours: + type: string + description: ์˜์—…์‹œ๊ฐ„ + example: "์›”-๊ธˆ 11:00-22:00, ํ† -์ผ 12:00-21:00" + createdAt: + type: string + format: date-time + description: ๊ฐ€์ž… ์ผ์‹œ + example: 2025-09-01T10:00:00Z + lastLoginAt: + type: string + format: date-time + description: ์ตœ์ข… ๋กœ๊ทธ์ธ ์ผ์‹œ + example: 2025-10-22T09:00:00Z + + UpdateProfileRequest: + type: object + properties: + name: + type: string + minLength: 2 + maxLength: 50 + description: ์‚ฌ์šฉ์ž ์ด๋ฆ„ (์„ ํƒ ์‚ฌํ•ญ) + example: ํ™๊ธธ๋™ + phoneNumber: + type: string + pattern: '^010\d{8}$' + description: ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ (์„ ํƒ ์‚ฌํ•ญ, ํ–ฅํ›„ ์žฌ์ธ์ฆ ํ•„์š”) + example: "01012345678" + email: + type: string + format: email + maxLength: 100 + description: ์ด๋ฉ”์ผ ์ฃผ์†Œ (์„ ํƒ ์‚ฌํ•ญ) + example: hong.new@example.com + storeName: + type: string + minLength: 2 + maxLength: 100 + description: ๋งค์žฅ๋ช… (์„ ํƒ ์‚ฌํ•ญ) + example: ๋ง›์žˆ๋Š”์ง‘ (๋ฆฌ๋‰ด์–ผ) + industry: + type: string + maxLength: 50 + description: ์—…์ข… (์„ ํƒ ์‚ฌํ•ญ) + example: ํ“จ์ „์Œ์‹์  + address: + type: string + minLength: 5 + maxLength: 200 + description: ๋งค์žฅ ์ฃผ์†Œ (์„ ํƒ ์‚ฌํ•ญ) + example: ์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 456 + businessHours: + type: string + maxLength: 200 + description: ์˜์—…์‹œ๊ฐ„ (์„ ํƒ ์‚ฌํ•ญ) + example: "๋งค์ผ 11:00-23:00" + + UpdateProfileResponse: + type: object + required: + - userId + - userName + - email + - storeId + - storeName + properties: + userId: + type: integer + format: int64 + description: ์‚ฌ์šฉ์ž ID + example: 123 + userName: + type: string + description: ์‚ฌ์šฉ์ž ์ด๋ฆ„ + example: ํ™๊ธธ๋™ + email: + type: string + format: email + description: ์ด๋ฉ”์ผ ์ฃผ์†Œ + example: hong.new@example.com + storeId: + type: integer + format: int64 + description: ๋งค์žฅ ID + example: 456 + storeName: + type: string + description: ๋งค์žฅ๋ช… + example: ๋ง›์žˆ๋Š”์ง‘ (๋ฆฌ๋‰ด์–ผ) + + ChangePasswordRequest: + type: object + required: + - currentPassword + - newPassword + properties: + currentPassword: + type: string + minLength: 8 + description: ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ + example: "Password123!" + newPassword: + type: string + minLength: 8 + maxLength: 100 + description: ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ (8์ž ์ด์ƒ, ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ) + example: "NewSecurePass456!" + + ChangePasswordResponse: + type: object + required: + - success + - message + properties: + success: + type: boolean + description: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์„ฑ๊ณต ์—ฌ๋ถ€ + example: true + message: + type: string + description: ์‘๋‹ต ๋ฉ”์‹œ์ง€ + example: ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค + + StoreDetailResponse: + type: object + required: + - userId + - storeId + - storeName + - industry + - address + properties: + userId: + type: integer + format: int64 + description: ์‚ฌ์šฉ์ž ID + example: 123 + storeId: + type: integer + format: int64 + description: ๋งค์žฅ ID + example: 456 + storeName: + type: string + description: ๋งค์žฅ๋ช… + example: ๋ง›์žˆ๋Š”์ง‘ + industry: + type: string + description: ์—…์ข… + example: ์Œ์‹์  + address: + type: string + description: ๋งค์žฅ ์ฃผ์†Œ + example: ์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ ํ…Œํ—ค๋ž€๋กœ 123 + businessHours: + type: string + description: ์˜์—…์‹œ๊ฐ„ + example: "์›”-๊ธˆ 11:00-22:00, ํ† -์ผ 12:00-21:00" + + ErrorResponse: + type: object + required: + - code + - message + - timestamp + properties: + code: + type: string + description: ์—๋Ÿฌ ์ฝ”๋“œ + example: USER_001 + enum: + - USER_001 # ์ค‘๋ณต ์‚ฌ์šฉ์ž + - USER_003 # ์‚ฌ์šฉ์ž ์—†์Œ + - USER_004 # ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ + - USER_005 # ๋™์‹œ์„ฑ ์ถฉ๋Œ + - AUTH_001 # ์ธ์ฆ ์‹คํŒจ + - AUTH_002 # ์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ + - AUTH_003 # ๊ถŒํ•œ ์—†์Œ (๋‚ด๋ถ€ ์„œ๋น„์Šค๋งŒ ์ ‘๊ทผ) + - VALIDATION_ERROR # ์ž…๋ ฅ ๊ฒ€์ฆ ์˜ค๋ฅ˜ + message: + type: string + description: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + example: ์ด๋ฏธ ๊ฐ€์ž…๋œ ์ „ํ™”๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค + timestamp: + type: string + format: date-time + description: ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ๊ฐ + example: 2025-10-22T10:30:00Z + details: + type: array + description: ์ƒ์„ธ ์—๋Ÿฌ ์ •๋ณด (์„ ํƒ ์‚ฌํ•ญ) + items: + type: string + example: ["ํ•„๋“œ๋ช…: ํ•„์ˆ˜ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค"] diff --git a/design/backend/logical/logical-architecture.md b/design/backend/logical/logical-architecture.md new file mode 100644 index 0000000..949ef44 --- /dev/null +++ b/design/backend/logical/logical-architecture.md @@ -0,0 +1,869 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜ + +## ๋ฌธ์„œ ์ •๋ณด +- **์ž‘์„ฑ์ผ**: 2025-10-21 +- **์ตœ์ข… ์ˆ˜์ •์ผ**: 2025-10-22 +- **๋ฒ„์ „**: 2.0 (CQRS + Event-Driven ์ „ํ™˜) +- **์ž‘์„ฑ์ž**: System Architect +- **๊ด€๋ จ ๋ฌธ์„œ**: + - [์œ ์ €์Šคํ† ๋ฆฌ](../../userstory.md) + - [์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด](../../pattern/architecture-pattern.md) + - [UI/UX ์„ค๊ณ„์„œ](../../uiux/uiux.md) + +## ๋ฒ„์ „ ์ด๋ ฅ +- **v1.0** (2025-10-21): ์ดˆ๊ธฐ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ +- **v2.0** (2025-10-22): CQRS ํŒจํ„ด ๋ฐ Event-Driven ์•„ํ‚คํ…์ฒ˜ ์ „ํ™˜, Resilience ํŒจํ„ด ์ „๋ฉด ์ ์šฉ +- **v2.1** (2025-10-22): ์„œ๋น„์Šค ๊ตฌ์กฐ ๊ฐ„์†Œํ™”, Kafka ํ†ตํ•ฉ (Event Bus + Job Queue), Distribution ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ +- **v2.2** (2025-10-22): Distribution Service ๋™๊ธฐ ํ˜ธ์ถœ ์ „ํ™˜ (REST API ์ง์ ‘ ํ˜ธ์ถœ) + +--- + +## ๋ชฉ์ฐจ +1. [๊ฐœ์š”](#1-๊ฐœ์š”) +2. [์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜](#2-์„œ๋น„์Šค-์•„ํ‚คํ…์ฒ˜) +3. [์ฃผ์š” ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ](#3-์ฃผ์š”-์‚ฌ์šฉ์ž-ํ”Œ๋กœ์šฐ) +4. [๋ฐ์ดํ„ฐ ํ๋ฆ„ ๋ฐ ์บ์‹ฑ ์ „๋žต](#4-๋ฐ์ดํ„ฐ-ํ๋ฆ„-๋ฐ-์บ์‹ฑ-์ „๋žต) +5. [ํ™•์žฅ์„ฑ ๋ฐ ์„ฑ๋Šฅ ๊ณ ๋ ค์‚ฌํ•ญ](#5-ํ™•์žฅ์„ฑ-๋ฐ-์„ฑ๋Šฅ-๊ณ ๋ ค์‚ฌํ•ญ) +6. [๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ](#6-๋ณด์•ˆ-๊ณ ๋ ค์‚ฌํ•ญ) +7. [๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜ ๋‹ค์ด์–ด๊ทธ๋žจ](#7-๋…ผ๋ฆฌ-์•„ํ‚คํ…์ฒ˜-๋‹ค์ด์–ด๊ทธ๋žจ) + +--- + +## 1. ๊ฐœ์š” + +### 1.1 ์„ค๊ณ„ ์›์น™ + +๋ณธ ๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜๋Š” ๋‹ค์Œ ์›์น™์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค: + +#### ์œ ์ €์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ์„ค๊ณ„ +- 20๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ์™€ ์ •ํ™•ํžˆ ๋งค์นญ +- ๋ถˆํ•„์š”ํ•œ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๋ฐฐ์ œ +- ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ ์šฐ์„  ๋ฐ˜์˜ + +#### Event-Driven ์•„ํ‚คํ…์ฒ˜ +- **Kafka ๊ธฐ๋ฐ˜ ํ†ตํ•ฉ**: Event Bus์™€ Job Queue๋ฅผ Kafka๋กœ ํ†ตํ•ฉ +- **๋น„๋™๊ธฐ ๋ฉ”์‹œ์ง•**: Kafka Topics๋ฅผ ํ†ตํ•œ ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  +- **๋А์Šจํ•œ ๊ฒฐํ•ฉ**: ์„œ๋น„์Šค ๊ฐ„ ์ง์ ‘ ์˜์กด์„ฑ ์ œ๊ฑฐ +- **ํ™•์žฅ์„ฑ**: ์ด๋ฒคํŠธ ๊ตฌ๋…์ž ์ถ”๊ฐ€๋กœ ๊ธฐ๋Šฅ ํ™•์žฅ ์šฉ์ด +- **์žฅ์•  ๊ฒฉ๋ฆฌ**: ์ด๋ฒคํŠธ ๋ฐœํ–‰/๊ตฌ๋… ์‹คํŒจ ์‹œ ์„œ๋น„์Šค ๋…๋ฆฝ์„ฑ ์œ ์ง€ + +#### Kafka ํ†ตํ•ฉ ์ „๋žต +- **Event Topics**: ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰/๊ตฌ๋… (EventCreated, ParticipantRegistered ๋“ฑ) +- **Job Topics**: ๋น„๋™๊ธฐ ์ž‘์—… ์š”์ฒญ/์ฒ˜๋ฆฌ (ai ์ด๋ฒคํŠธ ์ƒ์„ฑ, ์ด๋ฏธ์ง€ ์ƒ์„ฑ) +- **๋‹จ์ผ ๋ฉ”์‹œ์ง• ํ”Œ๋žซํผ**: ์šด์˜ ๋ณต์žก๋„ ๊ฐ์†Œ ๋ฐ ์ผ๊ด€๋œ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ + +#### Resilience ํŒจํ„ด ์ ์šฉ +- **Circuit Breaker**: ์™ธ๋ถ€ API ์žฅ์•  ์‹œ ๋น ๋ฅธ ์‹คํŒจ ๋ฐ ๋ณต๊ตฌ (Hystrix/Resilience4j) +- **Retry Pattern**: ์ผ์‹œ์  ์žฅ์•  ์‹œ ์ž๋™ ์žฌ์‹œ๋„ (์ง€์ˆ˜ ๋ฐฑ์˜คํ”„) +- **Timeout Pattern**: ์‘๋‹ต ์‹œ๊ฐ„ ์ œํ•œ์œผ๋กœ ๋ฆฌ์†Œ์Šค ์ ์œ  ๋ฐฉ์ง€ +- **Bulkhead Pattern**: ๋ฆฌ์†Œ์Šค ๊ฒฉ๋ฆฌ๋กœ ์žฅ์•  ์ „ํŒŒ ์ฐจ๋‹จ +- **Fallback Pattern**: ์žฅ์•  ์‹œ ๋Œ€์ฒด ๋กœ์ง ์‹คํ–‰ (์บ์‹œ ์‘๋‹ต ๋“ฑ) + +### 1.2 ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธ ์ •์˜ + +#### Core Services +1. **User Service**: ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๋งค์žฅ์ •๋ณด ๊ด€๋ฆฌ + - ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ (JWT ๋ฐœ๊ธ‰) + - ํ”„๋กœํ•„ CRUD + - Event Service๋กœ ํšŒ์›์ •๋ณด ์ œ๊ณต + +2. **Event Service**: ์ด๋ฒคํŠธ ์ „์ฒด ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ + - ์ด๋ฒคํŠธ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ/์กฐํšŒ + - ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ + - Kafka Job ๋ฐœํ–‰ (AI, ์ด๋ฏธ์ง€) + - Distribution Service ๋™๊ธฐ ํ˜ธ์ถœ (๋ฐฐํฌ) + - Kafka Event ๋ฐœํ–‰ (EventCreated) + +3. **Participation Service**: ์ฐธ์—ฌ ๋ฐ ๋‹น์ฒจ์ž ๊ด€๋ฆฌ + - ์ฐธ์—ฌ ์ ‘์ˆ˜ ๋ฐ ์ค‘๋ณต ์ฒดํฌ + - ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ + - ๋‹น์ฒจ์ž ์ถ”์ฒจ ๋ฐ ์กฐํšŒ + - Kafka Event ๋ฐœํ–‰ (ParticipantRegistered) + +4. **Analytics Service**: ์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ ๋ถ„์„ ๋ฐ ๋Œ€์‹œ๋ณด๋“œ + - ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ์กฐํšŒ (Redis ์บ์‹ฑ) + - Kafka Event ๊ตฌ๋… (EventCreated, ParticipantRegistered, DistributionCompleted) + - ์™ธ๋ถ€ ์ฑ„๋„ ํ†ต๊ณ„ ์ˆ˜์ง‘ (Circuit Breaker + Fallback) + - ROI ๊ณ„์‚ฐ ๋ฐ ์„ฑ๊ณผ ๋ถ„์„ + +#### Async Services (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) +1. **AI Service**: AI ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ถ”์ฒœ + - Kafka Job ๊ตฌ๋… (ai ์ด๋ฒคํŠธ ์ƒ์„ฑ) + - ์™ธ๋ถ€ AI API ํ˜ธ์ถœ (Circuit Breaker, Timeout 5๋ถ„) + - ๊ฒฐ๊ณผ Redis ์ €์žฅ (TTL 24์‹œ๊ฐ„) + +2. **Content Service**: SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ + - Redis์—์„œ AI ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ + - ์™ธ๋ถ€ ์ด๋ฏธ์ง€ ์ƒ์„ฑ API ํ˜ธ์ถœ (Circuit Breaker, Timeout 5๋ถ„) + - ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ Redis ์ €์žฅ (CDN URL, TTL 7์ผ) + +3. **Distribution Service**: ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ (๋™๊ธฐ) + - REST API ์ œ๊ณต (Event Service์—์„œ ํ˜ธ์ถœ) + - ๋ณ‘๋ ฌ ๋ฐฐํฌ (Circuit Breaker, Retry, Bulkhead) + - Kafka Event ๋ฐœํ–‰ (DistributionCompleted) + +#### Kafka (ํ†ตํ•ฉ ๋ฉ”์‹œ์ง• ํ”Œ๋žซํผ) +**Event Topics** (๋„๋ฉ”์ธ ์ด๋ฒคํŠธ): +- **EventCreated**: ์ด๋ฒคํŠธ ์ƒ์„ฑ ์‹œ +- **ParticipantRegistered**: ์ฐธ์—ฌ์ž ๋“ฑ๋ก ์‹œ +- **DistributionCompleted**: ๋ฐฐํฌ ์™„๋ฃŒ ์‹œ + +**Job Topics** (๋น„๋™๊ธฐ ์ž‘์—…): +- **ai ์ด๋ฒคํŠธ ์ƒ์„ฑ**: AI ์ถ”์ฒœ ์ž‘์—… +- **์ด๋ฏธ์ง€ ์ƒ์„ฑ**: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ž‘์—… + +**ํŠน์ง•**: +- At-Least-Once Delivery ๋ณด์žฅ +- Partition Key ๊ธฐ๋ฐ˜ ์ˆœ์„œ ๋ณด์žฅ +- Dead Letter Queue ์ง€์› + +#### Data Layer +- **Redis Cache**: ์„ธ์…˜, AI ๊ฒฐ๊ณผ, ์ด๋ฏธ์ง€ URL, ๋Œ€์‹œ๋ณด๋“œ ์บ์‹ฑ +- **PostgreSQL**: ์„œ๋น„์Šค๋ณ„ ๋…๋ฆฝ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค + - User DB, Event DB, Participation DB, Analytics DB + +#### External Systems +- **AI APIs**: Claude/GPT-4 (ํŠธ๋ Œ๋“œ ๋ถ„์„) +- **์ด๋ฏธ์ง€ ์ƒ์„ฑ APIs**: Stable Diffusion/DALL-E +- **๋ฐฐํฌ ์ฑ„๋„ APIs**: ์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS APIs (๋น„๋™๊ธฐ ๋ฐฐํฌ) + +--- + +## 2. ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ + +### 2.1 ์„œ๋น„์Šค๋ณ„ ์ฑ…์ž„ + +#### User Service +**ํ•ต์‹ฌ ์ฑ…์ž„**: +- ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ (JWT ํ† ํฐ ๋ฐœ๊ธ‰) +- ํ”„๋กœํ•„ CRUD (๋งค์žฅ ์ •๋ณด ํฌํ•จ) +- ์„ธ์…˜ ๊ด€๋ฆฌ +- Event Service๋กœ ํšŒ์›์ •๋ณด ์ œ๊ณต + +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-USER-010, 020, 030, 040 + +**์„œ๋น„์Šค ๊ฐ„ ํ˜ธ์ถœ**: +- **Event Service**: ํšŒ์›์ •๋ณด ์กฐํšŒ API ์ œ๊ณต (๋งค์žฅ ์ •๋ณด ํฌํ•จ) + +**๋ฐ์ดํ„ฐ ์ €์žฅ**: +- User DB: users, stores ํ…Œ์ด๋ธ” +- Redis: ์„ธ์…˜ ์ •๋ณด (TTL 7์ผ) + +#### Event Service +**ํ•ต์‹ฌ ์ฑ…์ž„**: +- ์ด๋ฒคํŠธ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ/์กฐํšŒ +- ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ +- Kafka Job ๋ฐœํ–‰ (AI, ์ด๋ฏธ์ง€, ๋ฐฐํฌ) +- Kafka Event ๋ฐœํ–‰ (EventCreated) + +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-EVENT-010, 020, 030, 040, 050, 060, 070 + +**Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰**: +1. **EventCreated**: ์ด๋ฒคํŠธ ์ƒ์„ฑ ์™„๋ฃŒ ์‹œ + - Payload: eventId, storeId, title, objective, createdAt + - ๊ตฌ๋…์ž: Analytics Service + +**Kafka Job ๋ฐœํ–‰**: +1. **ai ์ด๋ฒคํŠธ ์ƒ์„ฑ**: AI ์ถ”์ฒœ ์š”์ฒญ +2. **์ด๋ฏธ์ง€ ์ƒ์„ฑ**: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ + +**์„œ๋น„์Šค ๊ฐ„ ํ˜ธ์ถœ**: +- **Distribution Service**: ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ (๋™๊ธฐ ํ˜ธ์ถœ, Circuit Breaker ์ ์šฉ) + +**์ฃผ์š” ํ”Œ๋กœ์šฐ**: +1. ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ โ†’ Event DB ์ €์žฅ โ†’ EventCreated ๋ฐœํ–‰ +2. AI ์ถ”์ฒœ ์š”์ฒญ โ†’ ai ์ด๋ฒคํŠธ ์ƒ์„ฑ ๋ฐœํ–‰ +3. ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ โ†’ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ฐœํ–‰ +4. ๋ฐฐํฌ ์Šน์ธ โ†’ Distribution Service ๋™๊ธฐ ํ˜ธ์ถœ + +**๋ฐ์ดํ„ฐ ์ €์žฅ**: +- Event DB: events, event_objectives, event_prizes ํ…Œ์ด๋ธ” + +#### Participation Service +**ํ•ต์‹ฌ ์ฑ…์ž„**: +- ์ด๋ฒคํŠธ ์ฐธ์—ฌ ์ ‘์ˆ˜ ๋ฐ ๊ฒ€์ฆ +- ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ +- ๋‹น์ฒจ์ž ์ถ”์ฒจ ๋ฐ ์กฐํšŒ +- Kafka Event ๋ฐœํ–‰ (ParticipantRegistered) + +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-PART-010, 020, 030 + +**Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰**: +1. **ParticipantRegistered**: ์ฐธ์—ฌ์ž ๋“ฑ๋ก ์‹œ + - Payload: participantId, eventId, phoneNumber, registeredAt + - ๊ตฌ๋…์ž: Analytics Service + +**์ฃผ์š” ๊ธฐ๋Šฅ**: +- ์ค‘๋ณต ์ฐธ์—ฌ ์ฒดํฌ (์ „ํ™”๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜) +- ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ (ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ง€์›) +- ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฌด์ž‘์œ„ ์ถ”์ฒจ +- ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์  ์ ์šฉ +- ๋‹น์ฒจ์ž ์กฐํšŒ + +**๋ฐ์ดํ„ฐ ์ €์žฅ**: +- Participation DB: participants, winners ํ…Œ์ด๋ธ” + +#### Analytics Service +**ํ•ต์‹ฌ ์ฑ…์ž„**: +- ์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ +- ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ ๋ฐ ํ†ต๊ณ„ +- ROI ๊ณ„์‚ฐ ๋ฐ ์„ฑ๊ณผ ์ง‘๊ณ„ + +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-ANAL-010 + +**Kafka ์ด๋ฒคํŠธ ๊ตฌ๋…**: +- **EventCreated**: ์ด๋ฒคํŠธ ๊ธฐ๋ณธ ์ •๋ณด ์ดˆ๊ธฐํ™” +- **ParticipantRegistered**: ์ฐธ์—ฌ์ž ์ˆ˜ ์‹ค์‹œ๊ฐ„ ์ฆ๊ฐ€ +- **DistributionCompleted**: ๋ฐฐํฌ ์™„๋ฃŒ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ + +**Resilience ํŒจํ„ด**: +- **Circuit Breaker**: ์™ธ๋ถ€ ์ฑ„๋„ API ์กฐํšŒ ์‹œ (์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open) +- **Fallback**: ์บ์‹œ๋œ ์ด์ „ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ +- **Cache-Aside**: Redis ์บ์‹ฑ (TTL 5๋ถ„) + +**๋ฐ์ดํ„ฐ ํ†ตํ•ฉ**: +- Event Service: ์ด๋ฒคํŠธ ์ •๋ณด ์กฐํšŒ (DB ์ง์ ‘ ๋˜๋Š” REST) +- Participation Service: ์ฐธ์—ฌ์ž/๋‹น์ฒจ์ž ๋ฐ์ดํ„ฐ ์กฐํšŒ +- ์™ธ๋ถ€ APIs: ์šฐ๋ฆฌ๋™๋„คTV, ์ง€๋‹ˆTV, SNS ํ†ต๊ณ„ ์ˆ˜์ง‘ + +**๋ฐ์ดํ„ฐ ์ €์žฅ**: +- Analytics DB: event_stats, channel_stats +- Redis: ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ (TTL 5๋ถ„) + +### 2.2 Async Services (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) + +#### AI Service +**ํ•ต์‹ฌ ์ฑ…์ž„**: +- ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ ๋ถ„์„ +- 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ ์ž๋™ ์ƒ์„ฑ +- ์˜ˆ์ƒ ์„ฑ๊ณผ ๊ณ„์‚ฐ + +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-AI-010 + +**Kafka Job ๊ตฌ๋…**: +- **ai ์ด๋ฒคํŠธ ์ƒ์„ฑ**: AI ์ถ”์ฒœ ์ž‘์—… ์š”์ฒญ + +**Resilience ํŒจํ„ด**: +- **Circuit Breaker**: AI API ํ˜ธ์ถœ ์‹œ (์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open) +- **Timeout**: 5๋ถ„ (300์ดˆ) +- **Fallback**: ์บ์‹œ๋œ ์ด์ „ ์ถ”์ฒœ ๊ฒฐ๊ณผ + ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ +- **Cache-Aside**: Redis ์บ์‹ฑ (TTL 24์‹œ๊ฐ„) + +**์ฒ˜๋ฆฌ ์‹œ๊ฐ„**: +- ์บ์‹œ HIT: 0.1์ดˆ +- ์บ์‹œ MISS: 5๋ถ„ ์ด๋‚ด (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) + +**๋ฐ์ดํ„ฐ ์ €์žฅ**: +- Redis: AI ์ถ”์ฒœ ๊ฒฐ๊ณผ (TTL 24์‹œ๊ฐ„) +- Redis: Job ์ƒํƒœ ์ •๋ณด (TTL 1์‹œ๊ฐ„) + +#### Content Service +**ํ•ต์‹ฌ ์ฑ…์ž„**: +- 3๊ฐ€์ง€ ์Šคํƒ€์ผ SNS ์ด๋ฏธ์ง€ ์ž๋™ ์ƒ์„ฑ +- ํ”Œ๋žซํผ๋ณ„ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” +- ์ด๋ฏธ์ง€ ํŽธ์ง‘ ๊ธฐ๋Šฅ + +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-CONT-010, 020 + +**๋ฐ์ดํ„ฐ ์ฝ๊ธฐ**: +- Redis์—์„œ AI Service๊ฐ€ ์ €์žฅํ•œ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ + +**Resilience ํŒจํ„ด**: +- **Circuit Breaker**: ์ด๋ฏธ์ง€ ์ƒ์„ฑ API ํ˜ธ์ถœ ์‹œ (์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open) +- **Timeout**: 5๋ถ„ (300์ดˆ) +- **Fallback**: ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์ด๋ฏธ์ง€ ์ œ๊ณต +- **Cache-Aside**: Redis ์บ์‹ฑ (TTL 7์ผ) + +**์ฒ˜๋ฆฌ ์‹œ๊ฐ„**: +- ์บ์‹œ HIT: 0.1์ดˆ +- ์บ์‹œ MISS: 5๋ถ„ ์ด๋‚ด (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) + +**๋ฐ์ดํ„ฐ ์ €์žฅ**: +- Redis: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๊ฒฐ๊ณผ (CDN URL, TTL 7์ผ) +- CDN: ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ํŒŒ์ผ + +#### Distribution Service +**ํ•ต์‹ฌ ์ฑ…์ž„**: +- ๋‹ค์ค‘ ์ฑ„๋„ ๋ณ‘๋ ฌ ๋ฐฐํฌ (๋™๊ธฐ) +- ๋ฐฐํฌ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง +- Kafka Event ๋ฐœํ–‰ (DistributionCompleted) + +**๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-DIST-010, 020 + +**์ฃผ์š” API**: +- `POST /api/distribution/distribute`: ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ ์š”์ฒญ (Event Service์—์„œ ํ˜ธ์ถœ) + +**Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰**: +- **DistributionCompleted**: ๋ฐฐํฌ ์™„๋ฃŒ ์‹œ + - Payload: eventId, distributedChannels, completedAt + - ๊ตฌ๋…์ž: Analytics Service + +**Resilience ํŒจํ„ด**: +- **Circuit Breaker**: ๊ฐ ์™ธ๋ถ€ ์ฑ„๋„ API๋ณ„ ๋…๋ฆฝ ์ ์šฉ (์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open) +- **Retry**: ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„ (์ง€์ˆ˜ ๋ฐฑ์˜คํ”„: 1์ดˆ, 2์ดˆ, 4์ดˆ) +- **Bulkhead**: ์ฑ„๋„๋ณ„ ์Šค๋ ˆ๋“œ ํ’€ ๊ฒฉ๋ฆฌ (์žฅ์•  ์ „ํŒŒ ๋ฐฉ์ง€) +- **Fallback**: ์‹คํŒจ ์ฑ„๋„ ์Šคํ‚ต + ์•Œ๋ฆผ + +**์ฒ˜๋ฆฌ ์‹œ๊ฐ„**: 1๋ถ„ ์ด๋‚ด (๋ชจ๋“  ์ฑ„๋„ ๋ฐฐํฌ ์™„๋ฃŒ) + +**๋ฐฐํฌ ์ฑ„๋„**: +- ์šฐ๋ฆฌ๋™๋„คTV API (์˜์ƒ ์—…๋กœ๋“œ) +- ๋ง๊ณ ๋น„์ฆˆ API (์—ฐ๊ฒฐ์Œ ์—…๋ฐ์ดํŠธ) +- ์ง€๋‹ˆTV API (TV ๊ด‘๊ณ  ๋“ฑ๋ก) +- SNS APIs (Instagram, Naver, Kakao ์ž๋™ ํฌ์ŠคํŒ…) + +**๋ฐ์ดํ„ฐ ์ €์žฅ**: +- Event DB: distribution_logs ํ…Œ์ด๋ธ” + +### 2.3 Kafka ํ†ต์‹  ์ „๋žต + +#### Kafka ์•„ํ‚คํ…์ฒ˜ +**๊ธฐ์ˆ  ์Šคํƒ**: Apache Kafka (Event Topics + Job Topics ํ†ตํ•ฉ) +**๋ณด์žฅ ์ˆ˜์ค€**: At-Least-Once Delivery +**๋ฉ”์‹œ์ง€ ํฌ๋งท**: JSON + +#### Event Topics (๋„๋ฉ”์ธ ์ด๋ฒคํŠธ) + +| ํ† ํ”ฝ๋ช… | ๋ฐœํ–‰์ž | ๊ตฌ๋…์ž | Payload | ์šฉ๋„ | +|---------|--------|--------|---------|------| +| **EventCreated** | Event Service | Analytics Service | eventId, storeId, title, objective, createdAt | ์ด๋ฒคํŠธ ์ƒ์„ฑ ์‹œ ํ†ต๊ณ„ ์ดˆ๊ธฐํ™” | +| **ParticipantRegistered** | Participation Service | Analytics Service | participantId, eventId, phoneNumber, registeredAt | ์ฐธ์—ฌ์ž ๋“ฑ๋ก ์‹œ ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ | +| **DistributionCompleted** | Distribution Service | Analytics Service | eventId, distributedChannels, completedAt | ๋ฐฐํฌ ์™„๋ฃŒ ์‹œ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ | + +#### Job Topics (๋น„๋™๊ธฐ ์ž‘์—…) + +| ํ† ํ”ฝ๋ช… | ๋ฐœํ–‰์ž | ๊ตฌ๋…์ž | Payload | ์šฉ๋„ | +|---------|--------|--------|---------|------| +| **ai ์ด๋ฒคํŠธ ์ƒ์„ฑ** | Event Service | AI Service | eventId, objective, industry, region | AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ ์š”์ฒญ | +| **์ด๋ฏธ์ง€ ์ƒ์„ฑ** | Event Service | Content Service | eventId, content, style | SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ (3๊ฐ€์ง€ ์Šคํƒ€์ผ) | + +#### ํ†ต์‹  ํŒจํ„ด๋ณ„ ์„ค๊ณ„ + +**1. Event Topics (๋„๋ฉ”์ธ ์ด๋ฒคํŠธ)** +- **์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค**: ์„œ๋น„์Šค ๊ฐ„ ์ƒํƒœ ๋ณ€๊ฒฝ ์•Œ๋ฆผ ๋ฐ ๋™๊ธฐํ™” +- **ํ†ต์‹  ๋ฐฉ์‹**: Kafka Pub/Sub +- **์žฅ์ **: + - ์„œ๋น„์Šค ๋…๋ฆฝ์„ฑ ๋ณด์žฅ + - ์žฅ์•  ๊ฒฉ๋ฆฌ + - ํ™•์žฅ ์šฉ์ด +- **๋‹จ์ **: + - ์ตœ์ข… ์ผ๊ด€์„ฑ (Eventual Consistency) + - ๋””๋ฒ„๊น… ๋ณต์žก๋„ ์ฆ๊ฐ€ + +**2. Job Topics (๋น„๋™๊ธฐ ์ž‘์—…)** +- **์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค**: ์žฅ์‹œ๊ฐ„ ์ž‘์—… (AI ์ถ”์ฒœ, ์ด๋ฏธ์ง€ ์ƒ์„ฑ) +- **ํ†ต์‹  ๋ฐฉ์‹**: Kafka ๋ฉ”์‹œ์ง€ ํ +- **ํŒจํ„ด**: Asynchronous Request-Reply +- **์ฒ˜๋ฆฌ ํ”Œ๋กœ์šฐ**: + 1. Event Service โ†’ Kafka Job Topic: Job ๋ฐœํ–‰ + 2. Async Service โ†’ Kafka: Job ์ˆ˜์‹  ๋ฐ ์ฒ˜๋ฆฌ + 3. Client โ†’ Event Service: Job ์ƒํƒœ ํด๋ง (5์ดˆ ๊ฐ„๊ฒฉ) + 4. Async Service โ†’ Redis: ๊ฒฐ๊ณผ ์บ์‹ฑ + 5. Event Service โ†’ Client: ์™„๋ฃŒ ์‘๋‹ต + +**3. ์„œ๋น„์Šค ๊ฐ„ ๋™๊ธฐ ํ˜ธ์ถœ** +- **์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค**: ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ (Distribution Service) +- **ํ†ต์‹  ๋ฐฉ์‹**: REST API (HTTP/JSON) +- **ํŒจํ„ด**: Synchronous Request-Reply +- **์ฒ˜๋ฆฌ ํ”Œ๋กœ์šฐ**: + 1. Event Service โ†’ Distribution Service: POST /api/distribution/distribute + 2. Distribution Service: ๋‹ค์ค‘ ์ฑ„๋„ ๋ณ‘๋ ฌ ๋ฐฐํฌ (1๋ถ„ ์ด๋‚ด) + 3. Distribution Service โ†’ Event Service: ๋ฐฐํฌ ์™„๋ฃŒ ์‘๋‹ต + 4. Distribution Service โ†’ Kafka: DistributionCompleted ์ด๋ฒคํŠธ ๋ฐœํ–‰ +- **Resilience**: Circuit Breaker ์ ์šฉ (์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open) + +**4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ง์ ‘ ์กฐํšŒ** +- **์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค**: Analytics Service๊ฐ€ ์ด๋ฒคํŠธ/์ฐธ์—ฌ ๋ฐ์ดํ„ฐ ํ•„์š” ์‹œ +- **ํŒจํ„ด**: Database-per-Service ์›์น™ ์œ ์ง€, ํ•„์š” ์‹œ ์ด๋ฒคํŠธ๋กœ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” +- **ํ†ต์‹  ๋ฐฉ์‹**: Kafka ์ด๋ฒคํŠธ ๊ตฌ๋… โ†’ Analytics DB ์ €์žฅ โ†’ ๋กœ์ปฌ ์กฐํšŒ +- **ํŠน์ง•**: ์„œ๋น„์Šค ๊ฐ„ ์ง์ ‘ API ํ˜ธ์ถœ ์ตœ์†Œํ™” + +#### Cache-Aside ์ „๋žต + +| ์„œ๋น„์Šค | ์บ์‹œ ํ‚ค ํŒจํ„ด | TTL | ํžˆํŠธ์œจ ๋ชฉํ‘œ | ํšจ๊ณผ | +|--------|-------------|-----|-----------|------| +| AI Service | `ai:recommendation:{์—…์ข…}:{์ง€์—ญ}:{๋ชฉ์ }` | 24์‹œ๊ฐ„ | 80% | 10์ดˆ โ†’ 0.1์ดˆ (99% ๊ฐœ์„ ) | +| Content Service | `content:image:{์ด๋ฒคํŠธID}:{์Šคํƒ€์ผ}` | 7์ผ | 80% | 5์ดˆ โ†’ 0.1์ดˆ (98% ๊ฐœ์„ ) | +| User Service | `user:business:{์‚ฌ์—…์ž๋ฒˆํ˜ธ}` | 7์ผ | 90% | - | +| Analytics Query | `analytics:dashboard:{์ด๋ฒคํŠธID}` | 5๋ถ„ | 95% | 3์ดˆ โ†’ 0.5์ดˆ (83% ๊ฐœ์„ ) | + +#### Resilience ํŒจํ„ด ์ ์šฉ + +**1. Circuit Breaker ํŒจํ„ด** +- **์ ์šฉ ๋Œ€์ƒ**: ๋ชจ๋“  ์™ธ๋ถ€ API ํ˜ธ์ถœ +- **๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ**: Resilience4j ๋˜๋Š” Hystrix +- **์„ค์ •**: + ```yaml + circuit-breaker: + failure-rate-threshold: 50% # ์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open + slow-call-rate-threshold: 50% # ๋А๋ฆฐ ํ˜ธ์ถœ 50% ์ดˆ๊ณผ ์‹œ Open + slow-call-duration-threshold: 5s # 5์ดˆ ์ดˆ๊ณผ ์‹œ ๋А๋ฆฐ ํ˜ธ์ถœ๋กœ ๊ฐ„์ฃผ + wait-duration-in-open-state: 30s # Open ์ƒํƒœ 30์ดˆ ์œ ์ง€ ํ›„ Half-Open + permitted-calls-in-half-open: 3 # Half-Open ์ƒํƒœ์—์„œ 3๊ฐœ ์š”์ฒญ ํ…Œ์ŠคํŠธ + ``` + +**2. Retry ํŒจํ„ด** +- **์ ์šฉ ๋Œ€์ƒ**: ์ผ์‹œ์  ์žฅ์• ๊ฐ€ ์˜ˆ์ƒ๋˜๋Š” ์™ธ๋ถ€ API +- **์žฌ์‹œ๋„ ์ „๋žต**: ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ (Exponential Backoff) +- **์„ค์ •**: + ```yaml + retry: + max-attempts: 3 # ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„ + wait-duration: 1s # ์ดˆ๊ธฐ ๋Œ€๊ธฐ ์‹œ๊ฐ„ 1์ดˆ + exponential-backoff-multiplier: 2 # 2๋ฐฐ์”ฉ ์ฆ๊ฐ€ (1์ดˆ, 2์ดˆ, 4์ดˆ) + retry-exceptions: + - java.net.SocketTimeoutException + - java.net.ConnectException + ``` + +**3. Timeout ํŒจํ„ด** +- **์ ์šฉ ๋Œ€์ƒ**: ๋ชจ๋“  ์™ธ๋ถ€ API ํ˜ธ์ถœ +- **์„ค์ •**: + | ์„œ๋น„์Šค | Timeout | ์ด์œ  | + |--------|---------|------| + | AI Service (AI API) | 5๋ถ„ (300์ดˆ) | ๋ณต์žกํ•œ ๋ถ„์„ ์ž‘์—… | + | Content Service (์ด๋ฏธ์ง€ API) | 5๋ถ„ (300์ดˆ) | ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ๊ฐ„ ๊ณ ๋ ค | + | Distribution Service (์ฑ„๋„ APIs) | 10์ดˆ | ๋น ๋ฅธ ๋ฐฐํฌ ํ•„์š” | + +**4. Bulkhead ํŒจํ„ด** +- **์ ์šฉ ๋Œ€์ƒ**: Distribution Service (๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ) +- **๋ชฉ์ **: ์ฑ„๋„๋ณ„ ๋ฆฌ์†Œ์Šค ๊ฒฉ๋ฆฌ๋กœ ์žฅ์•  ์ „ํŒŒ ์ฐจ๋‹จ +- **์„ค์ •**: + ```yaml + bulkhead: + max-concurrent-calls: 10 # ์ฑ„๋„๋‹น ์ตœ๋Œ€ 10๊ฐœ ๋™์‹œ ํ˜ธ์ถœ + max-wait-duration: 0s # ๋Œ€๊ธฐ ์—†์ด ์ฆ‰์‹œ ์‹คํŒจ + ``` + +**5. Fallback ํŒจํ„ด** +- **์ ์šฉ ๋Œ€์ƒ**: ๋ชจ๋“  ์™ธ๋ถ€ API ํ˜ธ์ถœ +- **์ „๋žต**: + | ์„œ๋น„์Šค | Fallback ์ „๋žต | + |--------|---------------| + | AI Service | ์บ์‹œ๋œ ์ด์ „ ์ถ”์ฒœ ๊ฒฐ๊ณผ + ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ | + | Content Service | ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์ด๋ฏธ์ง€ ์ œ๊ณต | + | Distribution Service | ์‹คํŒจ ์ฑ„๋„ ์Šคํ‚ต + ์•Œ๋ฆผ | + | Analytics Service | ์บ์‹œ๋œ ์ด์ „ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ | + +#### ์ด๋ฒคํŠธ ์ˆœ์„œ ๋ณด์žฅ +- **Kafka Partition Key**: eventId ๊ธฐ์ค€์œผ๋กœ ํŒŒํ‹ฐ์…˜ ํ• ๋‹น +- **๋™์ผ ์ด๋ฒคํŠธ์˜ ๋ชจ๋“  ์ด๋ฒคํŠธ**: ๋™์ผ ํŒŒํ‹ฐ์…˜ โ†’ ์ˆœ์„œ ๋ณด์žฅ +- **๋‹ค๋ฅธ ์ด๋ฒคํŠธ**: ๋…๋ฆฝ์  ์ฒ˜๋ฆฌ โ†’ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ + +#### ์ด๋ฒคํŠธ ์žฌ์ฒ˜๋ฆฌ (At-Least-Once) +- **๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ**: ๊ตฌ๋…์ž๋Š” ๋™์ผ ์ด๋ฒคํŠธ ์ค‘๋ณต ์ฒ˜๋ฆฌ ์‹œ ๋ฉฑ๋“ฑ์„ฑ ์œ ์ง€ +- **๋ฐฉ๋ฒ•**: ์ด๋ฒคํŠธ ID ๊ธฐ๋ฐ˜ ์ค‘๋ณต ์ฒดํฌ (Redis Set ์‚ฌ์šฉ) + +--- + +## 3. ์ฃผ์š” ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ + +### 3.1 ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ (Event-Driven + Kafka) + +``` +1. [์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ] + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Client โ†’ Event Service โ”‚ + โ”‚ - POST /api/events (๋ชฉ์ , ๋งค์žฅ ์ •๋ณด) โ”‚ + โ”‚ - Event DB์— ์ €์žฅ โ”‚ + โ”‚ - EventCreated ์ด๋ฒคํŠธ ๋ฐœํ–‰ โ†’ Kafka โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Kafka โ†’ Analytics Service โ”‚ + โ”‚ - EventCreated ์ด๋ฒคํŠธ ๊ตฌ๋… โ”‚ + โ”‚ - Analytics DB์— ๊ธฐ๋ณธ ํ†ต๊ณ„ ์ดˆ๊ธฐํ™” โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +2. [AI ์ด๋ฒคํŠธ ์ถ”์ฒœ] + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Client โ†’ Event Service โ”‚ + โ”‚ - POST /api/events/{id}/ai-recommendations โ”‚ + โ”‚ - Kafka ai ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ† ํ”ฝ ๋ฐœํ–‰ (AI ์ž‘์—… ์š”์ฒญ) โ”‚ + โ”‚ - Job ID ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (0.1์ดˆ) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ AI Service (Background) โ”‚ + โ”‚ - Kafka ai ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ† ํ”ฝ ๊ตฌ๋… โ”‚ + โ”‚ - Redis ์บ์‹œ ํ™•์ธ (Cache-Aside) โ”‚ + โ”‚ - ์บ์‹œ MISS: Claude API ํ˜ธ์ถœ (5๋ถ„) [Circuit Breaker] โ”‚ + โ”‚ - AI ์ถ”์ฒœ ๊ฒฐ๊ณผ๋ฅผ Redis์— ์ €์žฅ (TTL 24์‹œ๊ฐ„) โ”‚ + โ”‚ - Job ์ƒํƒœ ์™„๋ฃŒ๋กœ ์—…๋ฐ์ดํŠธ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Client (Polling) โ”‚ + โ”‚ - GET /api/jobs/{id} (5์ดˆ ๊ฐ„๊ฒฉ) โ”‚ + โ”‚ - ์™„๋ฃŒ ์‹œ: AI ์ถ”์ฒœ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ (3๊ฐ€์ง€ ์˜ต์…˜) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +3. [SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ] + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Content Service (Background) โ”‚ + โ”‚ - Redis์—์„œ AI Service๊ฐ€ ์ €์žฅํ•œ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ โ”‚ + โ”‚ - Redis ์บ์‹œ ํ™•์ธ (์ด๋ฏธ์ง€ ์ƒ์„ฑ ์—ฌ๋ถ€) โ”‚ + โ”‚ - ์บ์‹œ MISS: Stable Diffusion API (5๋ถ„) [Circuit Breaker] โ”‚ + โ”‚ - ์ด๋ฏธ์ง€ CDN ์—…๋กœ๋“œ โ”‚ + โ”‚ - ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ URL์„ Redis์— ์ €์žฅ (TTL 7์ผ) โ”‚ + โ”‚ - Job ์ƒํƒœ ์™„๋ฃŒ๋กœ ์—…๋ฐ์ดํŠธ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Client (Polling) โ”‚ + โ”‚ - GET /api/jobs/{id} (3์ดˆ ๊ฐ„๊ฒฉ) โ”‚ + โ”‚ - ์™„๋ฃŒ ์‹œ: 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์ด๋ฏธ์ง€ URL ๋ฐ˜ํ™˜ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +4. [์ตœ์ข… ์Šน์ธ ๋ฐ ๋ฐฐํฌ] + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Client โ†’ Event Service โ”‚ + โ”‚ - POST /api/events/{id}/publish โ”‚ + โ”‚ - Redis์˜ ์ด๋ฒคํŠธ ๊ด€๋ จ ์ •๋ณด(AI ์ถ”์ฒœ, ์ด๋ฏธ์ง€ URL)๋ฅผ ์กฐํšŒ โ”‚ + โ”‚ - Event DB์— ์ด๋ฒคํŠธ ์ •๋ณด ์ €์žฅ โ”‚ + โ”‚ - Event ์ƒํƒœ ๋ณ€๊ฒฝ (DRAFT โ†’ PUBLISHED) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Event Service โ†’ Distribution Service โ”‚ + โ”‚ - POST /api/distribution/distribute (๋™๊ธฐ ํ˜ธ์ถœ) โ”‚ + โ”‚ - ๋‹ค์ค‘ ์ฑ„๋„ ๋ณ‘๋ ฌ ๋ฐฐํฌ [Circuit Breaker + Bulkhead] โ”‚ + โ”‚ * ์šฐ๋ฆฌ๋™๋„คTV API (์˜์ƒ ์—…๋กœ๋“œ) [Retry: 3ํšŒ] โ”‚ + โ”‚ * ๋ง๊ณ ๋น„์ฆˆ API (์—ฐ๊ฒฐ์Œ ์—…๋ฐ์ดํŠธ) [Retry: 3ํšŒ] โ”‚ + โ”‚ * ์ง€๋‹ˆTV API (๊ด‘๊ณ  ๋“ฑ๋ก) [Retry: 3ํšŒ] โ”‚ + โ”‚ * SNS APIs (Instagram, Naver, Kakao) [Retry: 3ํšŒ] โ”‚ + โ”‚ - ๋ฐฐํฌ ์™„๋ฃŒ: DistributionCompleted ์ด๋ฒคํŠธ ๋ฐœํ–‰ โ†’ Kafka โ”‚ + โ”‚ - Event Service๋กœ ๋ฐฐํฌ ์™„๋ฃŒ ์‘๋‹ต (1๋ถ„ ์ด๋‚ด) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Kafka โ†’ Analytics Service โ”‚ + โ”‚ - DistributionCompleted ์ด๋ฒคํŠธ ๊ตฌ๋… โ”‚ + โ”‚ - Analytics DB ๋ฐฐํฌ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ โ”‚ + โ”‚ - ๋Œ€์‹œ๋ณด๋“œ ์บ์‹œ ๋ฌดํšจํ™” (๋‹ค์Œ ์กฐํšŒ ์‹œ ๊ฐฑ์‹ ) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Event Service โ†’ Client โ”‚ + โ”‚ - ๋ฐฐํฌ ์™„๋ฃŒ ์‘๋‹ต ๋ฐ˜ํ™˜ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 3.2 ๊ณ ๊ฐ ์ฐธ์—ฌ ํ”Œ๋กœ์šฐ (Event-Driven) + +``` +1. [์ด๋ฒคํŠธ ์ฐธ์—ฌ] + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Client โ†’ Participation Service โ”‚ + โ”‚ - POST /api/events/{id}/participate โ”‚ + โ”‚ - ์ค‘๋ณต ์ฐธ์—ฌ ์ฒดํฌ (์ „ํ™”๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜) โ”‚ + โ”‚ - Participation DB์— ์ €์žฅ โ”‚ + โ”‚ - ParticipantRegistered ์ด๋ฒคํŠธ ๋ฐœํ–‰ โ†’ Kafka โ”‚ + โ”‚ - ์‘๋ชจ ๋ฒˆํ˜ธ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Kafka โ†’ Analytics Service โ”‚ + โ”‚ - ParticipantRegistered ์ด๋ฒคํŠธ ๊ตฌ๋… โ”‚ + โ”‚ - ์‹ค์‹œ๊ฐ„ ์ฐธ์—ฌ์ž ์ˆ˜ ์ฆ๊ฐ€ โ”‚ + โ”‚ - Analytics DB์— ์ฐธ์—ฌ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ โ”‚ + โ”‚ - ๋Œ€์‹œ๋ณด๋“œ ์บ์‹œ ๋ฌดํšจํ™” โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +2. [๋‹น์ฒจ์ž ์ถ”์ฒจ] + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Client โ†’ Participation Service โ”‚ + โ”‚ - POST /api/events/{id}/draw-winners โ”‚ + โ”‚ - ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฌด์ž‘์œ„ ์ถ”์ฒจ โ”‚ + โ”‚ - Winners DB์— ์ €์žฅ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 3.3 ์„ฑ๊ณผ ๋ถ„์„ ํ”Œ๋กœ์šฐ (Event-Driven) + +``` +1. [์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ] + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Client โ†’ Analytics Service โ”‚ + โ”‚ - GET /api/events/{id}/analytics โ”‚ + โ”‚ - Redis ์บ์‹œ ํ™•์ธ (TTL 5๋ถ„) โ”‚ + โ”‚ * ์บ์‹œ HIT: ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (0.5์ดˆ) โ”‚ + โ”‚ * ์บ์‹œ MISS: ์•„๋ž˜ ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Analytics Service (๋ฐ์ดํ„ฐ ํ†ตํ•ฉ) โ”‚ + โ”‚ - Analytics DB: ์ด๋ฒคํŠธ/์ฐธ์—ฌ ํ†ต๊ณ„ ์กฐํšŒ (๋กœ์ปฌ DB) โ”‚ + โ”‚ - ์™ธ๋ถ€ APIs: ์ฑ„๋„๋ณ„ ๋…ธ์ถœ/ํด๋ฆญ ์ˆ˜ [Circuit Breaker + Fallback] โ”‚ + โ”‚ * ์šฐ๋ฆฌ๋™๋„คTV API (์กฐํšŒ์ˆ˜) โ”‚ + โ”‚ * ์ง€๋‹ˆTV API (๊ด‘๊ณ  ๋…ธ์ถœ ์ˆ˜) โ”‚ + โ”‚ * SNS APIs (์ข‹์•„์š”, ๋Œ“๊ธ€, ๊ณต์œ  ์ˆ˜) โ”‚ + โ”‚ - Redis ์บ์‹ฑ (TTL 5๋ถ„) โ”‚ + โ”‚ - ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +2. [์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (Event ๊ตฌ๋…)] + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Analytics Service (Background) โ”‚ + โ”‚ - EventCreated ๊ตฌ๋…: ์ด๋ฒคํŠธ ๊ธฐ๋ณธ ์ •๋ณด ์ดˆ๊ธฐํ™” โ”‚ + โ”‚ - ParticipantRegistered ๊ตฌ๋…: ์ฐธ์—ฌ์ž ์ˆ˜ ์‹ค์‹œ๊ฐ„ ์ฆ๊ฐ€ โ”‚ + โ”‚ - DistributionCompleted ๊ตฌ๋…: ๋ฐฐํฌ ์ฑ„๋„ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ โ”‚ + โ”‚ - ์บ์‹œ ๋ฌดํšจํ™”: ๋‹ค์Œ ์กฐํšŒ ์‹œ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 3.4 ํ”Œ๋กœ์šฐ ํŠน์ง• + +#### Kafka ํ†ตํ•ฉ ์ด์  +- **๋‹จ์ผ ๋ฉ”์‹œ์ง• ํ”Œ๋žซํผ**: Event Bus์™€ Job Queue๋ฅผ Kafka๋กœ ํ†ตํ•ฉ, ์šด์˜ ๋ณต์žก๋„ ๊ฐ์†Œ +- **์ผ๊ด€๋œ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ**: ๋ชจ๋“  ๋น„๋™๊ธฐ ํ†ต์‹ ์ด Kafka๋ฅผ ํ†ตํ•ด ์ด๋ฃจ์–ด์ ธ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋””๋ฒ„๊น… ์šฉ์ด +- **ํ™•์žฅ์„ฑ**: Kafka์˜ ๋†’์€ ์ฒ˜๋ฆฌ๋Ÿ‰์œผ๋กœ ๋Œ€๊ทœ๋ชจ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ง€์› + +#### Event-Driven ์ด์  +- **๋А์Šจํ•œ ๊ฒฐํ•ฉ**: ์„œ๋น„์Šค ๊ฐ„ ์ง์ ‘ ์˜์กด์„ฑ ์ œ๊ฑฐ +- **์žฅ์•  ๊ฒฉ๋ฆฌ**: ํ•œ ์„œ๋น„์Šค ์žฅ์• ๊ฐ€ ๋‹ค๋ฅธ ์„œ๋น„์Šค์— ์˜ํ–ฅ ์—†์Œ +- **ํ™•์žฅ ์šฉ์ด**: ์ƒˆ๋กœ์šด ๊ตฌ๋…์ž ์ถ”๊ฐ€๋กœ ๊ธฐ๋Šฅ ํ™•์žฅ +- **๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ**: ์‚ฌ์šฉ์ž ์‘๋‹ต ์‹œ๊ฐ„ ๋‹จ์ถ• + +#### Resilience ์ด์  +- **Circuit Breaker**: ์™ธ๋ถ€ API ์žฅ์•  ์‹œ ๋น ๋ฅธ ์‹คํŒจ ๋ฐ ๋ณต๊ตฌ +- **Retry**: ์ผ์‹œ์  ์žฅ์•  ์ž๋™ ๋ณต๊ตฌ +- **Fallback**: ์žฅ์•  ์‹œ์—๋„ ์„œ๋น„์Šค ์ง€์† (Graceful Degradation) +- **Bulkhead**: ๋ฆฌ์†Œ์Šค ๊ฒฉ๋ฆฌ๋กœ ์žฅ์•  ์ „ํŒŒ ์ฐจ๋‹จ + +--- + +## 4. ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๋ฐ ์บ์‹ฑ ์ „๋žต + +### 4.1 ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +#### ์ฝ๊ธฐ ํ”Œ๋กœ์šฐ (Cache-Aside ํŒจํ„ด) +``` +1. Application โ†’ Cache ํ™•์ธ + - Cache HIT: ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + - Cache MISS: + 2. Application โ†’ Database/External API ์กฐํšŒ + 3. Database/External API โ†’ Application ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + 4. Application โ†’ Cache ๋ฐ์ดํ„ฐ ์ €์žฅ (TTL ์„ค์ •) + 5. Application โ†’ Client ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ +``` + +#### ์“ฐ๊ธฐ ํ”Œ๋กœ์šฐ (Write-Through ํŒจํ„ด) +``` +1. Application โ†’ Database ์“ฐ๊ธฐ +2. Database โ†’ Application ์„ฑ๊ณต ์‘๋‹ต +3. Application โ†’ Cache ๋ฌดํšจํ™” ๋˜๋Š” ์—…๋ฐ์ดํŠธ +4. Application โ†’ Client ์„ฑ๊ณต ์‘๋‹ต +``` + +### 4.2 ์บ์‹ฑ ์ „๋žต + +#### Redis ์บ์‹œ ๊ตฌ์กฐ + +| ์„œ๋น„์Šค | ์บ์‹œ ํ‚ค ํŒจํ„ด | ๋ฐ์ดํ„ฐ ํƒ€์ž… | TTL | ์˜ˆ์ƒ ํฌ๊ธฐ | ํžˆํŠธ์œจ ๋ชฉํ‘œ | +|--------|-------------|-----------|-----|----------|-----------| +| User | `user:session:{token}` | String | 7์ผ | 1KB | - | +| AI | `ai:recommendation:{์—…์ข…}:{์ง€์—ญ}:{๋ชฉ์ }` | Hash | 24์‹œ๊ฐ„ | 10KB | 80% | +| AI | `ai:event:{์ด๋ฒคํŠธID}` | Hash | 24์‹œ๊ฐ„ | 10KB | - | +| Content | `content:image:{์ด๋ฒคํŠธID}:{์Šคํƒ€์ผ}` | String | 7์ผ | 0.2KB (URL) | 80% | +| Analytics | `analytics:dashboard:{์ด๋ฒคํŠธID}` | Hash | 5๋ถ„ | 5KB | 95% | +| AI | `job:{jobId}` | Hash | 1์‹œ๊ฐ„ | 1KB | - | +| Content | `job:{jobId}` | Hash | 1์‹œ๊ฐ„ | 1KB | - | + +#### Redis ๋ฉ”๋ชจ๋ฆฌ ์‚ฐ์ • +- **์˜ˆ์ƒ ๋™์‹œ ์‚ฌ์šฉ์ž**: 100๋ช… +- **์˜ˆ์ƒ ์ด๋ฒคํŠธ ์ˆ˜**: 50๊ฐœ +- **์˜ˆ์ƒ ์บ์‹œ ํ•ญ๋ชฉ ์ˆ˜**: 10,000๊ฐœ +- **์˜ˆ์ƒ ์ด ๋ฉ”๋ชจ๋ฆฌ**: ์•ฝ 50MB (์šด์˜ ํ™˜๊ฒฝ 2GB ํ• ๋‹น) + +#### ์บ์‹œ ๋ฌดํšจํ™” ์ „๋žต +- **TTL ๊ธฐ๋ฐ˜ ์ž๋™ ๋งŒ๋ฃŒ**: ๋Œ€๋ถ€๋ถ„์˜ ์บ์‹œ +- **์ˆ˜๋™ ๋ฌดํšจํ™”**: ์ด๋ฒคํŠธ ์ˆ˜์ •/์‚ญ์ œ ์‹œ ๊ด€๋ จ ์บ์‹œ ์‚ญ์ œ +- **Lazy ๋ฌดํšจํ™”**: ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๋‹ค์Œ ์กฐํšŒ ์‹œ์ ์— ๊ฐฑ์‹  + +### 4.3 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ „๋žต + +#### ์„œ๋น„์Šค๋ณ„ ๋…๋ฆฝ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค +- **User DB**: users, stores +- **Event DB**: events, event_objectives, event_prizes, distribution_logs +- **Participation DB**: participants, winners +- **Analytics DB**: event_stats, channel_stats + +#### ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์ „๋žต +- **Eventual Consistency**: ์„œ๋น„์Šค ๊ฐ„ ๋ฐ์ดํ„ฐ๋Š” ์ตœ์ข… ์ผ๊ด€์„ฑ ๋ณด์žฅ +- **Strong Consistency**: ์„œ๋น„์Šค ๋‚ด๋ถ€ ํŠธ๋žœ์žญ์…˜์€ ๊ฐ•ํ•œ ์ผ๊ด€์„ฑ ๋ณด์žฅ +- **Saga ํŒจํ„ด**: ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ (๋ณด์ƒ ํŠธ๋žœ์žญ์…˜) + +--- + +## 5. ํ™•์žฅ์„ฑ ๋ฐ ์„ฑ๋Šฅ ๊ณ ๋ ค์‚ฌํ•ญ + +### 5.1 ์ˆ˜ํ‰ ํ™•์žฅ ์ „๋žต + +#### ์„œ๋น„์Šค๋ณ„ ํ™•์žฅ ์ „๋žต +| ์„œ๋น„์Šค | ์ดˆ๊ธฐ ์ธ์Šคํ„ด์Šค | ํ™•์žฅ ์กฐ๊ฑด | ์ตœ๋Œ€ ์ธ์Šคํ„ด์Šค | Auto-scaling ๋ฉ”ํŠธ๋ฆญ | +|--------|-------------|----------|-------------|-------------------| +| User | 2 | CPU > 70% | 5 | CPU, ๋ฉ”๋ชจ๋ฆฌ | +| Event | 2 | CPU > 70% | 10 | CPU, ๋ฉ”๋ชจ๋ฆฌ | +| AI | 1 | Job Queue > 10 | 3 | Queue ๊ธธ์ด | +| Content | 1 | Job Queue > 10 | 3 | Queue ๊ธธ์ด | +| Distribution | 2 | CPU > 70% | 5 | CPU, ๋ฉ”๋ชจ๋ฆฌ | +| Participation | 1 | CPU > 70% | 3 | CPU, ๋ฉ”๋ชจ๋ฆฌ | +| Analytics | 1 | CPU > 70% | 3 | CPU, ๋ฉ”๋ชจ๋ฆฌ | + +#### Redis Cluster +- **์ดˆ๊ธฐ ๊ตฌ์„ฑ**: 3 ๋…ธ๋“œ (Master 3, Replica 3) +- **ํ™•์žฅ**: ๋…ธ๋“œ ์ถ”๊ฐ€๋ฅผ ํ†ตํ•œ ์ˆ˜ํ‰ ํ™•์žฅ +- **HA**: Redis Sentinel์„ ํ†ตํ•œ ์ž๋™ Failover + +#### Database Replication +- **Primary-Replica ๊ตฌ์กฐ**: ์ฝ๊ธฐ ๋ถ€ํ•˜ ๋ถ„์‚ฐ +- **์ฝ๊ธฐ ํ™•์žฅ**: Read Replica ์ถ”๊ฐ€ (ํ•„์š” ์‹œ) +- **์“ฐ๊ธฐ ํ™•์žฅ**: Sharding (Phase 2 ์ดํ›„) + +### 5.2 ์„ฑ๋Šฅ ๋ชฉํ‘œ + +#### ์‘๋‹ต ์‹œ๊ฐ„ ๋ชฉํ‘œ +| ๊ธฐ๋Šฅ | ๋ชฉํ‘œ ์‹œ๊ฐ„ | ์บ์‹œ HIT | ์บ์‹œ MISS | +|------|----------|---------|----------| +| ๋กœ๊ทธ์ธ | 0.5์ดˆ | - | - | +| ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ | 0.3์ดˆ | - | - | +| AI ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ถ”์ฒœ | 0.1์ดˆ | โœ… | 10์ดˆ (๋น„๋™๊ธฐ) | +| SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ | 0.1์ดˆ | โœ… | 5์ดˆ (๋น„๋™๊ธฐ) | +| ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ | 1๋ถ„ | - | - | +| ๋Œ€์‹œ๋ณด๋“œ ๋กœ๋”ฉ | 0.5์ดˆ | โœ… | 3์ดˆ | + +#### ์ฒ˜๋ฆฌ๋Ÿ‰ ๋ชฉํ‘œ +- **๋™์‹œ ์‚ฌ์šฉ์ž**: 100๋ช… (MVP ๋ชฉํ‘œ) +- **API ์š”์ฒญ**: 1,000 req/min +- **AI ์ž‘์—…**: 10 jobs/min +- **์ด๋ฏธ์ง€ ์ƒ์„ฑ**: 10 jobs/min + +### 5.3 ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ธฐ๋ฒ• + +#### Frontend ์ตœ์ ํ™” +- **Code Splitting**: ํŽ˜์ด์ง€๋ณ„ ๋ฒˆ๋“ค ๋ถ„ํ•  +- **Lazy Loading**: ์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ง€์—ฐ ๋กœ๋”ฉ +- **CDN**: ์ •์  ์ž์‚ฐ CDN ๋ฐฐํฌ +- **Compression**: Gzip/Brotli ์••์ถ• + +#### Backend ์ตœ์ ํ™” +- **Connection Pooling**: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ’€ ๊ด€๋ฆฌ +- **Query Optimization**: ์ธ๋ฑ์Šค ์ตœ์ ํ™”, N+1 ์ฟผ๋ฆฌ ๋ฐฉ์ง€ +- **Batch Processing**: ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ผ๊ด„ ์ฒ˜๋ฆฌ +- **Pagination**: ๋ชฉ๋ก ์กฐํšŒ ํŽ˜์ด์ง€๋„ค์ด์…˜ + +#### Cache ์ตœ์ ํ™” +- **Multi-Level Caching**: Browser Cache โ†’ CDN โ†’ Redis โ†’ Database +- **Cache Warming**: ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ๋ฐ์ดํ„ฐ ์‚ฌ์ „ ๋กœ๋”ฉ +- **Cache Preloading**: ํ”ผํฌ ์‹œ๊ฐ„ ์ „ ์บ์‹œ ์ค€๋น„ + +--- + +## 6. ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ + +### 6.1 ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ + +#### JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ +- **ํ† ํฐ ๋ฐœ๊ธ‰**: User Service์—์„œ ๋กœ๊ทธ์ธ ์‹œ JWT ํ† ํฐ ๋ฐœ๊ธ‰ +- **ํ† ํฐ ๊ฒ€์ฆ**: API Gateway์—์„œ ๋ชจ๋“  ์š”์ฒญ์˜ JWT ํ† ํฐ ๊ฒ€์ฆ +- **ํ† ํฐ ์ €์žฅ**: Redis์— ์„ธ์…˜ ์ •๋ณด ์ €์žฅ (TTL 7์ผ) +- **ํ† ํฐ ๊ฐฑ์‹ **: Refresh Token ํŒจํ„ด (์„ ํƒ) + +#### ์—ญํ•  ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด (RBAC) +- **์—ญํ• **: OWNER (๋งค์žฅ ์‚ฌ์žฅ๋‹˜), CUSTOMER (์ด๋ฒคํŠธ ์ฐธ์—ฌ์ž) +- **๊ถŒํ•œ ๊ด€๋ฆฌ**: API๋ณ„ ํ•„์š” ์—ญํ•  ์ •์˜ +- **API Gateway ๊ฒ€์ฆ**: ์š”์ฒญ์ž์˜ ์—ญํ•  ํ™•์ธ + +### 6.2 ๋ฐ์ดํ„ฐ ๋ณด์•ˆ + +#### ๋ฏผ๊ฐ ์ •๋ณด ์•”ํ˜ธํ™” +- **๋น„๋ฐ€๋ฒˆํ˜ธ**: bcrypt ํ•ด์‹ฑ (Cost Factor: 10) +- **์‚ฌ์—…์ž๋ฒˆํ˜ธ**: AES-256 ์•”ํ˜ธํ™” ์ €์žฅ +- **๊ฐœ์ธ์ •๋ณด**: ์ „ํ™”๋ฒˆํ˜ธ ๋งˆ์Šคํ‚น (010-****-1234) + +#### ์ „์†ก ๋ณด์•ˆ +- **HTTPS**: ๋ชจ๋“  ํ†ต์‹  TLS 1.3 ์•”ํ˜ธํ™” +- **API Key**: ์™ธ๋ถ€ API ํ˜ธ์ถœ ์‹œ ์•ˆ์ „ํ•œ Key ๊ด€๋ฆฌ (AWS Secrets Manager) + +#### ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ํ†ต์ œ +- **Database**: ์„œ๋น„์Šค๋ณ„ ๋…๋ฆฝ ๊ณ„์ •, ์ตœ์†Œ ๊ถŒํ•œ ์›์น™ +- **Redis**: ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ •, ACL ์ ์šฉ +- **๋ฐฑ์—…**: ์•”ํ˜ธํ™”๋œ ๋ฐฑ์—… ์ €์žฅ + +### 6.3 ๋ณด์•ˆ ๋ชจ๋‹ˆํ„ฐ๋ง + +#### ์œ„ํ˜‘ ํƒ์ง€ +- **Rate Limiting**: API Gateway์—์„œ ์‚ฌ์šฉ์ž๋‹น 100 req/min +- **Brute Force ๋ฐฉ์ง€**: ๋กœ๊ทธ์ธ 5ํšŒ ์‹คํŒจ ์‹œ ๊ณ„์ • ์ž ๊ธˆ (์‚ญ์ œ๋จ, ํ–ฅํ›„ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ) +- **SQL Injection ๋ฐฉ์ง€**: Prepared Statement ์‚ฌ์šฉ +- **XSS ๋ฐฉ์ง€**: ์ž…๋ ฅ ๋ฐ์ดํ„ฐ Sanitization + +#### ๋กœ๊น… ๋ฐ ๊ฐ์‚ฌ +- **Access Log**: ๋ชจ๋“  API ์š”์ฒญ ๋กœ๊น… +- **Audit Log**: ๋ฏผ๊ฐ ์ž‘์—… (๋กœ๊ทธ์ธ, ์ด๋ฒคํŠธ ์ƒ์„ฑ, ๋‹น์ฒจ์ž ์ถ”์ฒจ) ๊ฐ์‚ฌ ๋กœ๊ทธ +- **์ค‘์•™์ง‘์ค‘์‹ ๋กœ๊น…**: ELK Stack ๋˜๋Š” CloudWatch Logs + +--- + +## 7. ๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜ ๋‹ค์ด์–ด๊ทธ๋žจ + +๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜ ๋‹ค์ด์–ด๊ทธ๋žจ์€ ๋ณ„๋„ Mermaid ํŒŒ์ผ๋กœ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + +**ํŒŒ์ผ ์œ„์น˜**: `logical-architecture.mmd` + +**๋‹ค์ด์–ด๊ทธ๋žจ ํ™•์ธ ๋ฐฉ๋ฒ•**: +1. https://mermaid.live/edit ์ ‘์† +2. `logical-architecture.mmd` ํŒŒ์ผ ๋‚ด์šฉ ๋ถ™์—ฌ๋„ฃ๊ธฐ +3. ๋‹ค์ด์–ด๊ทธ๋žจ ์‹œ๊ฐ์  ํ™•์ธ + +**๋‹ค์ด์–ด๊ทธ๋žจ ๊ตฌ์„ฑ**: +- Services: 4๊ฐœ ํ•ต์‹ฌ ์„œ๋น„์Šค (User, Event, Participation, Analytics) +- Async Services: 3๊ฐœ ๋น„๋™๊ธฐ ์„œ๋น„์Šค (AI, Content, Distribution) +- Kafka: Event Topics + Job Topics ํ†ตํ•ฉ ๋ฉ”์‹œ์ง• ํ”Œ๋žซํผ +- External System: ํ†ตํ•ฉ๋œ ์™ธ๋ถ€ ์‹œ์Šคํ…œ (๊ตญ์„ธ์ฒญ API, AI API, ์ด๋ฏธ์ง€ ์ƒ์„ฑ API, ๋ฐฐํฌ ์ฑ„๋„ APIs) + +**์˜์กด์„ฑ ํ‘œํ˜„**: +- ๊ตต์€ ํ™”์‚ดํ‘œ (==>): Kafka Event Topics ๋ฐœํ–‰ +- ์‹ค์„  ํ™”์‚ดํ‘œ (-->): Kafka Job Topics ๋ฐœํ–‰ ๋˜๋Š” ์™ธ๋ถ€ ์‹œ์Šคํ…œ ํ˜ธ์ถœ +- ์ ์„  ํ™”์‚ดํ‘œ (-.->): Kafka ๊ตฌ๋… + +--- + +## ๋ถ€๋ก + +### A. ์ฐธ๊ณ  ๋ฌธ์„œ +- [์œ ์ €์Šคํ† ๋ฆฌ](../../userstory.md) +- [์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด](../../pattern/architecture-pattern.md) +- [UI/UX ์„ค๊ณ„์„œ](../../uiux/uiux.md) +- [ํด๋ผ์šฐ๋“œ ๋””์ž์ธ ํŒจํ„ด](../../../claude/cloud-design-patterns.md) + +### B. ์ฃผ์š” ๊ฒฐ์ •์‚ฌํ•ญ +1. **Kafka ํ†ตํ•ฉ ๋ฉ”์‹œ์ง• ํ”Œ๋žซํผ ์ฑ„ํƒ**: Event Bus์™€ Job Queue๋ฅผ Kafka๋กœ ํ†ตํ•ฉํ•˜์—ฌ ์šด์˜ ๋ณต์žก๋„ ๊ฐ์†Œ +2. **Event-Driven ์•„ํ‚คํ…์ฒ˜ ์ฑ„ํƒ**: Kafka๋ฅผ ํ†ตํ•œ ์„œ๋น„์Šค ๊ฐ„ ๋А์Šจํ•œ ๊ฒฐํ•ฉ ๋ฐ ๋น„๋™๊ธฐ ํ†ต์‹  +3. **๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ์ •์˜**: 3๊ฐœ Event Topics (EventCreated, ParticipantRegistered, DistributionCompleted) +4. **Job Topics ์ •์˜**: 2๊ฐœ Job Topics (ai ์ด๋ฒคํŠธ ์ƒ์„ฑ, ์ด๋ฏธ์ง€ ์ƒ์„ฑ)๋กœ ์žฅ์‹œ๊ฐ„ ๋น„๋™๊ธฐ ์ž‘์—… ์ฒ˜๋ฆฌ +5. **Resilience ํŒจํ„ด ์ „๋ฉด ์ ์šฉ**: Circuit Breaker, Retry, Timeout, Bulkhead, Fallback +6. **At-Least-Once Delivery**: Kafka ๋ฉ”์‹œ์ง€ ๋ณด์žฅ ๋ฐ ๋ฉฑ๋“ฑ์„ฑ ์„ค๊ณ„ +7. **Cache-Aside ํŒจํ„ด**: AI/์ด๋ฏธ์ง€ ์ƒ์„ฑ ๊ฒฐ๊ณผ ์บ์‹ฑ์œผ๋กœ ์‘๋‹ต ์‹œ๊ฐ„ 90% ๊ฐœ์„  +8. **Redis ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ณต์œ **: AI Service โ†’ Redis โ†’ Content Service ๋ฐ์ดํ„ฐ ํ๋ฆ„ +9. **Redis to DB ์˜๊ตฌ ์ €์žฅ**: ์ด๋ฒคํŠธ ์ƒ์„ฑ ์™„๋ฃŒ ์‹œ Redis ๋ฐ์ดํ„ฐ๋ฅผ Event DB์— ์ €์žฅ +10. **๋™๊ธฐ ๋ฐฐํฌ**: Event Service๊ฐ€ Distribution Service๋ฅผ REST API๋กœ ์ง์ ‘ ํ˜ธ์ถœํ•˜์—ฌ ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ ๋™๊ธฐ ์ฒ˜๋ฆฌ +11. **์„œ๋น„์Šค๋ณ„ ๋…๋ฆฝ Database**: Database-per-Service ํŒจํ„ด์œผ๋กœ ์„œ๋น„์Šค ๋…๋ฆฝ์„ฑ ๋ณด์žฅ +12. **์žฅ์‹œ๊ฐ„ ์ž‘์—… Timeout ์กฐ์ •**: AI/Content Service Timeout์„ 5๋ถ„์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ๋ณต์žกํ•œ ์ƒ์„ฑ ์ž‘์—… ์ง€์› + +### C. ํ–ฅํ›„ ๊ฐœ์„  ๋ฐฉ์•ˆ (Phase 2 ์ดํ›„) +1. **Event Sourcing ์™„์ „ ์ ์šฉ**: ๋ชจ๋“  ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์ด๋ฒคํŠธ๋กœ ์ €์žฅํ•˜์—ฌ ์‹œ๊ฐ„ ์—ฌํ–‰ ๋ฐ ๊ฐ์‚ฌ ์ถ”์  ๊ฐ•ํ™” +2. **Saga ํŒจํ„ด ์ ์šฉ**: ๋ณต์žกํ•œ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ๋ณด์ƒ ๋กœ์ง ์ฒด๊ณ„ํ™” +3. **Service Mesh ๋„์ž…**: Istio๋ฅผ ํ†ตํ•œ ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  ๊ด€์ฐฐ์„ฑ ๋ฐ ๋ณด์•ˆ ๊ฐ•ํ™” +4. **Database Sharding**: Event/Participation Write DB ์ƒค๋”ฉ์œผ๋กœ ์“ฐ๊ธฐ ํ™•์žฅ์„ฑ ๊ฐœ์„  +5. **WebSocket ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ํ‘ธ์‹œ**: ๋Œ€์‹œ๋ณด๋“œ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (ํด๋ง ๋Œ€์ฒด) +6. **GraphQL API Gateway**: ํด๋ผ์ด์–ธํŠธ ๋งž์ถคํ˜• ๋ฐ์ดํ„ฐ ์กฐํšŒ ์ตœ์ ํ™” +7. **Dead Letter Queue ๊ณ ๋„ํ™”**: ์‹คํŒจ ์ด๋ฒคํŠธ ์žฌ์ฒ˜๋ฆฌ ๋ฐ ์•Œ๋ฆผ ์ž๋™ํ™” + +--- + +**๋ฌธ์„œ ๋ฒ„์ „**: 2.2 +**์ตœ์ข… ์ˆ˜์ •์ผ**: 2025-10-22 +**์ž‘์„ฑ์ž**: System Architect +**๋ณ€๊ฒฝ ์‚ฌํ•ญ**: Distribution Service ๋™๊ธฐ ํ˜ธ์ถœ ์ „ํ™˜ (REST API ์ง์ ‘ ํ˜ธ์ถœ) diff --git a/design/backend/logical/logical-architecture.mmd b/design/backend/logical/logical-architecture.mmd new file mode 100644 index 0000000..d84f619 --- /dev/null +++ b/design/backend/logical/logical-architecture.mmd @@ -0,0 +1,71 @@ +graph TB + %% KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜ (Event-Driven + Kafka) + + %% Services + subgraph "Services" + UserSvc["User Service
โ€ข ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ
โ€ข ํ”„๋กœํ•„ ๊ด€๋ฆฌ
โ€ข ํšŒ์›์ •๋ณด ์ œ๊ณต"] + EventSvc["Event Service
โ€ข ์ด๋ฒคํŠธ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ
โ€ข ํ”Œ๋กœ์šฐ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜
โ€ข AI ์ž‘์—… ์š”์ฒญ
โ€ข ๋ฐฐํฌ ์ž‘์—… ์š”์ฒญ
โ€ข Redis โ†’ DB ์ €์žฅ"] + PartSvc["Participation
Service
โ€ข ์ฐธ์—ฌ ์ ‘์ˆ˜
โ€ข ์ฐธ์—ฌ์ž ๋ชฉ๋ก
โ€ข ๋‹น์ฒจ์ž ์ถ”์ฒจ"] + AnalSvc["Analytics Service
โ€ข ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ
โ€ข ์„ฑ๊ณผ ๋ถ„์„
โ€ข ์ฑ„๋„๋ณ„ ํ†ต๊ณ„
[Circuit Breaker]"] + end + + %% Async Services + subgraph "Async Services" + AISvc["AI Service
โ€ข ํŠธ๋ Œ๋“œ ๋ถ„์„
โ€ข ์ด๋ฒคํŠธ ์ถ”์ฒœ
โ€ข Redis ์ €์žฅ
[Circuit Breaker]
[Timeout: 5๋ถ„]"] + ContentSvc["Content Service
โ€ข Redis ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ
โ€ข SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ
โ€ข Redis ์ €์žฅ
[Circuit Breaker]
[Timeout: 5๋ถ„]"] + DistSvc["Distribution
Service
โ€ข ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ
[Circuit Breaker]
[Retry: 3ํšŒ]
[Bulkhead]"] + end + + %% Kafka (Event Bus + Job Queue) + Kafka["Kafka
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”

โ€ข EventCreated
โ€ข ParticipantRegistered
โ€ข DistributionCompleted
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”

โ€ข ai ์ด๋ฒคํŠธ ์ƒ์„ฑ"] + + %% External System + External["์™ธ๋ถ€์‹œ์Šคํ…œ
[Circuit Breaker]
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
โ€ข AI API
โ€ข ์ด๋ฏธ์ง€ ์ƒ์„ฑ API
โ€ข ๋ฐฐํฌ ์ฑ„๋„ APIs
(๋น„๋™๊ธฐ)"] + + %% Redis + Redis["Redis Cache
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
โ€ข AI ๊ฒฐ๊ณผ
โ€ข ์ด๋ฏธ์ง€ URL
โ€ข ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ"] + + %% Event Publishing + EventSvc ==>|"EventCreated
๋ฐœํ–‰"| Kafka + PartSvc ==>|"ParticipantRegistered
๋ฐœํ–‰"| Kafka + DistSvc ==>|"DistributionCompleted
๋ฐœํ–‰"| Kafka + + %% Job Publishing (๋น„๋™๊ธฐ ์ž‘์—… ์š”์ฒญ) + EventSvc -->|"ai ์ด๋ฒคํŠธ ์ƒ์„ฑ ๋ฐœํ–‰"| Kafka + + %% Event Subscription + Kafka -.->|"EventCreated
๊ตฌ๋…"| AnalSvc + Kafka -.->|"ParticipantRegistered
๊ตฌ๋…"| AnalSvc + Kafka -.->|"DistributionCompleted
๊ตฌ๋…"| AnalSvc + + %% Job Subscription + Kafka -.->|"ai ์ด๋ฒคํŠธ ์ƒ์„ฑ ๊ตฌ๋…"| AISvc + + %% Service to Service (๋™๊ธฐ ํ˜ธ์ถœ) + EventSvc -->|"๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ
[Circuit Breaker]"| DistSvc + EventSvc -->|"ํšŒ์›์ •๋ณด ์กฐํšŒ"| UserSvc + + %% Redis Interactions + AISvc -->|"AI ๊ฒฐ๊ณผ ์ €์žฅ"| Redis + ContentSvc -->|"AI ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ"| Redis + ContentSvc -->|"์ด๋ฏธ์ง€ URL ์ €์žฅ"| Redis + EventSvc -->|"Redis โ†’ DB ์ €์žฅ"| Redis + + %% Services to External (Resilience ํŒจํ„ด) + AISvc -->|"ํŠธ๋ Œ๋“œ ๋ถ„์„/์ถ”์ฒœ"| External + ContentSvc -->|"์ด๋ฏธ์ง€ ์ƒ์„ฑ"| External + DistSvc -->|"๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ
(๋น„๋™๊ธฐ)"| External + AnalSvc -->|"์ฑ„๋„๋ณ„ ํ†ต๊ณ„
[Fallback: Cache]"| External + + %% Styling + classDef service fill:#4ECDC4,stroke:#14B8A6,stroke-width:3px + classDef async fill:#8B5CF6,stroke:#7C3AED,stroke-width:3px,color:#fff + classDef kafka fill:#F59E0B,stroke:#D97706,stroke-width:3px + classDef external fill:#E5E7EB,stroke:#9CA3AF,stroke-width:2px + classDef cache fill:#EF4444,stroke:#DC2626,stroke-width:3px + + class UserSvc,EventSvc,PartSvc,AnalSvc service + class AISvc,ContentSvc,DistSvc async + class Kafka kafka + class External external + class Redis cache diff --git a/design/backend/sequence/inner/README.md b/design/backend/sequence/inner/README.md new file mode 100644 index 0000000..ac0f380 --- /dev/null +++ b/design/backend/sequence/inner/README.md @@ -0,0 +1,393 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ๋‚ด๋ถ€ ์‹œํ€€์Šค ์„ค๊ณ„์„œ + +## ๋ฌธ์„œ ์ •๋ณด +- **์ž‘์„ฑ์ผ**: 2025-10-22 +- **๋ฒ„์ „**: 1.0 +- **์ž‘์„ฑ์ž**: System Architect +- **๊ด€๋ จ ๋ฌธ์„œ**: + - [์œ ์ €์Šคํ† ๋ฆฌ](../../userstory.md) + - [์™ธ๋ถ€ ์‹œํ€€์Šค ์„ค๊ณ„์„œ](../outer/) + - [๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜](../../logical/logical-architecture.md) + +--- + +## ๋ชฉ์ฐจ +1. [๊ฐœ์š”](#1-๊ฐœ์š”) +2. [์„œ๋น„์Šค๋ณ„ ์‹œ๋‚˜๋ฆฌ์˜ค ๋ชฉ๋ก](#2-์„œ๋น„์Šค๋ณ„-์‹œ๋‚˜๋ฆฌ์˜ค-๋ชฉ๋ก) +3. [์„ค๊ณ„ ์›์น™](#3-์„ค๊ณ„-์›์น™) +4. [์ฃผ์š” ํŒจํ„ด](#4-์ฃผ์š”-ํŒจํ„ด) +5. [ํŒŒ์ผ ๊ตฌ์กฐ](#5-ํŒŒ์ผ-๊ตฌ์กฐ) +6. [PlantUML ๋‹ค์ด์–ด๊ทธ๋žจ ํ™•์ธ ๋ฐฉ๋ฒ•](#6-plantuml-๋‹ค์ด์–ด๊ทธ๋žจ-ํ™•์ธ-๋ฐฉ๋ฒ•) + +--- + +## 1. ๊ฐœ์š” + +๋ณธ ๋ฌธ์„œ๋Š” KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค์˜ **7๊ฐœ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค**์— ๋Œ€ํ•œ **26๊ฐœ ๋‚ด๋ถ€ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ**์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + +### 1.1 ์„ค๊ณ„ ๋ฒ”์œ„ + +๊ฐ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๋‚ด๋ถ€์˜ ์ฒ˜๋ฆฌ ํ๋ฆ„์„ ์ƒ์„ธํžˆ ํ‘œํ˜„: +- **API ๋ ˆ์ด์–ด**: Controller +- **๋น„์ฆˆ๋‹ˆ์Šค ๋ ˆ์ด์–ด**: Service, Validator, Domain Logic +- **๋ฐ์ดํ„ฐ ๋ ˆ์ด์–ด**: Repository, Cache Manager +- **์ธํ”„๋ผ ๋ ˆ์ด์–ด**: Kafka, Redis, Database, External APIs + +### 1.2 ์„ค๊ณ„ ๋Œ€์ƒ ์„œ๋น„์Šค + +| ์„œ๋น„์Šค | ์‹œ๋‚˜๋ฆฌ์˜ค ์ˆ˜ | ์ฃผ์š” ์ฑ…์ž„ | +|--------|------------|----------| +| **User** | 4 | ์‚ฌ์šฉ์ž ์ธ์ฆ, ํ”„๋กœํ•„ ๊ด€๋ฆฌ | +| **Event** | 10 | ์ด๋ฒคํŠธ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ, ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ | +| **Participation** | 3 | ์ฐธ์—ฌ์ž ๊ด€๋ฆฌ, ๋‹น์ฒจ์ž ์ถ”์ฒจ | +| **Analytics** | 5 | ์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ ๋ถ„์„, ๋Œ€์‹œ๋ณด๋“œ | +| **AI** | 1 | AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ | +| **Content** | 1 | SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ | +| **Distribution** | 2 | ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ | +| **์ด๊ณ„** | **26** | - | + +--- + +## 2. ์„œ๋น„์Šค๋ณ„ ์‹œ๋‚˜๋ฆฌ์˜ค ๋ชฉ๋ก + +### 2.1 User ์„œ๋น„์Šค (4๊ฐœ) + +| ์‹œ๋‚˜๋ฆฌ์˜ค | ํŒŒ์ผ๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ฃผ์š” ์ฒ˜๋ฆฌ ๋‚ด์šฉ | +|----------|--------|-----------|---------------| +| ํšŒ์›๊ฐ€์ž… | `user-ํšŒ์›๊ฐ€์ž….puml` | UFR-USER-010 | ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ(Circuit Breaker), ํŠธ๋žœ์žญ์…˜, JWT ๋ฐœ๊ธ‰ | +| ๋กœ๊ทธ์ธ | `user-๋กœ๊ทธ์ธ.puml` | UFR-USER-020 | ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ(bcrypt), JWT ๋ฐœ๊ธ‰, ์„ธ์…˜ ์ €์žฅ | +| ํ”„๋กœํ•„์ˆ˜์ • | `user-ํ”„๋กœํ•„์ˆ˜์ •.puml` | UFR-USER-030 | ๊ธฐ๋ณธ/๋งค์žฅ ์ •๋ณด ์ˆ˜์ •, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ, ํŠธ๋žœ์žญ์…˜ | +| ๋กœ๊ทธ์•„์›ƒ | `user-๋กœ๊ทธ์•„์›ƒ.puml` | UFR-USER-040 | JWT ๊ฒ€์ฆ, ์„ธ์…˜ ์‚ญ์ œ, Blacklist ์ถ”๊ฐ€ | + +**์ฃผ์š” ํŠน์ง•**: +- **Resilience ํŒจํ„ด**: Circuit Breaker (๊ตญ์„ธ์ฒญ API), Retry, Timeout, Fallback +- **๋ณด์•ˆ**: bcrypt ํ•ด์‹ฑ, AES-256 ์•”ํ˜ธํ™”, JWT ๊ด€๋ฆฌ +- **์บ์‹ฑ**: ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ๊ฒฐ๊ณผ (TTL 7์ผ), ์„ธ์…˜ ์ •๋ณด (TTL 7์ผ) + +--- + +### 2.2 Event ์„œ๋น„์Šค (10๊ฐœ) + +| ์‹œ๋‚˜๋ฆฌ์˜ค | ํŒŒ์ผ๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ฃผ์š” ์ฒ˜๋ฆฌ ๋‚ด์šฉ | +|----------|--------|-----------|---------------| +| ๋ชฉ์ ์„ ํƒ | `event-๋ชฉ์ ์„ ํƒ.puml` | UFR-EVENT-020 | ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ ๋ฐ ์ €์žฅ, EventCreated ๋ฐœํ–‰ | +| AI์ถ”์ฒœ์š”์ฒญ | `event-AI์ถ”์ฒœ์š”์ฒญ.puml` | UFR-EVENT-030 | Kafka ai-job ๋ฐœํ–‰, Job ID ๋ฐ˜ํ™˜ (202 Accepted) | +| ์ถ”์ฒœ๊ฒฐ๊ณผ์กฐํšŒ | `event-์ถ”์ฒœ๊ฒฐ๊ณผ์กฐํšŒ.puml` | UFR-EVENT-030 | Redis Job ์ƒํƒœ ํด๋ง ์กฐํšŒ | +| ์ด๋ฏธ์ง€์ƒ์„ฑ์š”์ฒญ | `event-์ด๋ฏธ์ง€์ƒ์„ฑ์š”์ฒญ.puml` | UFR-CONT-010 | Kafka image-job ๋ฐœํ–‰, Job ID ๋ฐ˜ํ™˜ (202 Accepted) | +| ์ด๋ฏธ์ง€๊ฒฐ๊ณผ์กฐํšŒ | `event-์ด๋ฏธ์ง€๊ฒฐ๊ณผ์กฐํšŒ.puml` | UFR-CONT-010 | Redis Job ์ƒํƒœ ํด๋ง ์กฐํšŒ | +| ์ฝ˜ํ…์ธ ์„ ํƒ | `event-์ฝ˜ํ…์ธ ์„ ํƒ.puml` | UFR-CONT-020 | ์„ ํƒํ•œ ์ฝ˜ํ…์ธ  ์ €์žฅ | +| ์ตœ์ข…์Šน์ธ๋ฐ๋ฐฐํฌ | `event-์ตœ์ข…์Šน์ธ๋ฐ๋ฐฐํฌ.puml` | UFR-EVENT-050 | Distribution Service ๋™๊ธฐ ํ˜ธ์ถœ, ์ƒํƒœ ๋ณ€๊ฒฝ | +| ์ƒ์„ธ์กฐํšŒ | `event-์ƒ์„ธ์กฐํšŒ.puml` | UFR-EVENT-060 | ์ด๋ฒคํŠธ ์ƒ์„ธ ์กฐํšŒ (์บ์‹ฑ) | +| ๋ชฉ๋ก์กฐํšŒ | `event-๋ชฉ๋ก์กฐํšŒ.puml` | UFR-EVENT-070 | ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ (ํ•„ํ„ฐ/๊ฒ€์ƒ‰/ํŽ˜์ด์ง€๋„ค์ด์…˜) | +| ๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ | `event-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml` | UFR-EVENT-010 | ๋Œ€์‹œ๋ณด๋“œ ์ด๋ฒคํŠธ ๋ชฉ๋ก (๋ณ‘๋ ฌ ์ฟผ๋ฆฌ) | + +**์ฃผ์š” ํŠน์ง•**: +- **Kafka ํ†ตํ•ฉ**: Event Topics (EventCreated), Job Topics (ai-job, image-job) +- **๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ**: Job ๋ฐœํ–‰ โ†’ ํด๋ง ๋ฐฉ์‹ ๊ฒฐ๊ณผ ์กฐํšŒ +- **๋™๊ธฐ ํ˜ธ์ถœ**: Distribution Service REST API ์ง์ ‘ ํ˜ธ์ถœ +- **์บ์‹ฑ ์ „๋žต**: ๋ชฉ์ (30๋ถ„), ์ƒ์„ธ(5๋ถ„), ๋ชฉ๋ก/๋Œ€์‹œ๋ณด๋“œ(1๋ถ„) + +--- + +### 2.3 Participation ์„œ๋น„์Šค (3๊ฐœ) + +| ์‹œ๋‚˜๋ฆฌ์˜ค | ํŒŒ์ผ๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ฃผ์š” ์ฒ˜๋ฆฌ ๋‚ด์šฉ | +|----------|--------|-----------|---------------| +| ์ด๋ฒคํŠธ์ฐธ์—ฌ | `participation-์ด๋ฒคํŠธ์ฐธ์—ฌ.puml` | UFR-PART-010 | ์ค‘๋ณต ์ฒดํฌ, ParticipantRegistered ๋ฐœํ–‰ | +| ์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ | `participation-์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ.puml` | UFR-PART-020 | ํ•„ํ„ฐ/๊ฒ€์ƒ‰, ํŽ˜์ด์ง€๋„ค์ด์…˜, ์ „ํ™”๋ฒˆํ˜ธ ๋งˆ์Šคํ‚น | +| ๋‹น์ฒจ์ž์ถ”์ฒจ | `participation-๋‹น์ฒจ์ž์ถ”์ฒจ.puml` | UFR-PART-030 | Fisher-Yates Shuffle, WinnerSelected ๋ฐœํ–‰ | + +**์ฃผ์š” ํŠน์ง•**: +- **์ค‘๋ณต ๋ฐฉ์ง€**: Redis Cache + DB 2๋‹จ๊ณ„ ์ฒดํฌ +- **์ถ”์ฒจ ์•Œ๊ณ ๋ฆฌ์ฆ˜**: ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๊ณต์ •์„ฑ, ๊ฐ€์‚ฐ์  ์‹œ์Šคํ…œ, Fisher-Yates Shuffle +- **Kafka Event**: ParticipantRegistered, WinnerSelected โ†’ Analytics Service ๊ตฌ๋… +- **๋ณด์•ˆ**: ์ „ํ™”๋ฒˆํ˜ธ ๋งˆ์Šคํ‚น (010-****-1234) + +--- + +### 2.4 Analytics ์„œ๋น„์Šค (5๊ฐœ) + +| ์‹œ๋‚˜๋ฆฌ์˜ค | ํŒŒ์ผ๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ฃผ์š” ์ฒ˜๋ฆฌ ๋‚ด์šฉ | +|----------|--------|-----------|---------------| +| ๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ-์บ์‹œํžˆํŠธ | `analytics-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ-์บ์‹œํžˆํŠธ.puml` | UFR-ANAL-010 | Redis ์บ์‹œ HIT (0.5์ดˆ) | +| ๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ-์บ์‹œ๋ฏธ์Šค | `analytics-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ-์บ์‹œ๋ฏธ์Šค.puml` | UFR-ANAL-010 | ์™ธ๋ถ€ API ๋ณ‘๋ ฌ ํ˜ธ์ถœ, ROI ๊ณ„์‚ฐ (3์ดˆ) | +| ์ด๋ฒคํŠธ์ƒ์„ฑ๊ตฌ๋… | `analytics-์ด๋ฒคํŠธ์ƒ์„ฑ๊ตฌ๋….puml` | - | EventCreated ๊ตฌ๋…, ํ†ต๊ณ„ ์ดˆ๊ธฐํ™” | +| ์ฐธ์—ฌ์ž๋“ฑ๋ก๊ตฌ๋… | `analytics-์ฐธ์—ฌ์ž๋“ฑ๋ก๊ตฌ๋….puml` | - | ParticipantRegistered ๊ตฌ๋…, ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ | +| ๋ฐฐํฌ์™„๋ฃŒ๊ตฌ๋… | `analytics-๋ฐฐํฌ์™„๋ฃŒ๊ตฌ๋….puml` | - | DistributionCompleted ๊ตฌ๋…, ๋ฐฐํฌ ํ†ต๊ณ„ | + +**์ฃผ์š” ํŠน์ง•**: +- **Cache-Aside ํŒจํ„ด**: Redis ์บ์‹ฑ (TTL 5๋ถ„, ํžˆํŠธ์œจ 95%) +- **์™ธ๋ถ€ API ๋ณ‘๋ ฌ ํ˜ธ์ถœ**: ์šฐ๋ฆฌ๋™๋„คTV, ์ง€๋‹ˆTV, SNS APIs (Circuit Breaker, Timeout, Fallback) +- **Kafka ๊ตฌ๋…**: 3๊ฐœ Event Topics ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ +- **๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ**: Redis Set์œผ๋กœ ์ค‘๋ณต ์ด๋ฒคํŠธ ๋ฐฉ์ง€ + +--- + +### 2.5 AI ์„œ๋น„์Šค (1๊ฐœ) + +| ์‹œ๋‚˜๋ฆฌ์˜ค | ํŒŒ์ผ๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ฃผ์š” ์ฒ˜๋ฆฌ ๋‚ด์šฉ | +|----------|--------|-----------|---------------| +| ํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ถ”์ฒœ | `ai-ํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ถ”์ฒœ.puml` | UFR-AI-010 | Kafka ai-job ๊ตฌ๋…, ํŠธ๋ Œ๋“œ ๋ถ„์„, 3๊ฐ€์ง€ ์ถ”์ฒœ ๋ณ‘๋ ฌ ์ƒ์„ฑ | + +**์ฃผ์š” ํŠน์ง•**: +- **Kafka Job ๊ตฌ๋…**: ai-job ํ† ํ”ฝ Consumer +- **์™ธ๋ถ€ AI API**: Claude/GPT-4 ํ˜ธ์ถœ (Circuit Breaker, Timeout 30์ดˆ) +- **์บ์‹ฑ ์ „๋žต**: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ (TTL 1์‹œ๊ฐ„), ์ถ”์ฒœ ๊ฒฐ๊ณผ (TTL 24์‹œ๊ฐ„) +- **3๊ฐ€์ง€ ์˜ต์…˜ ๋ณ‘๋ ฌ ์ƒ์„ฑ**: ์ €๋น„์šฉ/์ค‘๋น„์šฉ/๊ณ ๋น„์šฉ ์ถ”์ฒœ์•ˆ + +--- + +### 2.6 Content ์„œ๋น„์Šค (1๊ฐœ) + +| ์‹œ๋‚˜๋ฆฌ์˜ค | ํŒŒ์ผ๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ฃผ์š” ์ฒ˜๋ฆฌ ๋‚ด์šฉ | +|----------|--------|-----------|---------------| +| ์ด๋ฏธ์ง€์ƒ์„ฑ | `content-์ด๋ฏธ์ง€์ƒ์„ฑ.puml` | UFR-CONT-010 | Kafka image-job ๊ตฌ๋…, 3๊ฐ€์ง€ ์Šคํƒ€์ผ ๋ณ‘๋ ฌ ์ƒ์„ฑ | + +**์ฃผ์š” ํŠน์ง•**: +- **Kafka Job ๊ตฌ๋…**: image-job ํ† ํ”ฝ Consumer +- **์™ธ๋ถ€ ์ด๋ฏธ์ง€ API**: Stable Diffusion/DALL-E ๋ณ‘๋ ฌ ํ˜ธ์ถœ (Circuit Breaker, Timeout 20์ดˆ) +- **3๊ฐ€์ง€ ์Šคํƒ€์ผ ๋ณ‘๋ ฌ**: ์‹ฌํ”Œ/ํ™”๋ คํ•œ/ํŠธ๋ Œ๋”” (par ๋ธ”๋ก) +- **CDN ์—…๋กœ๋“œ**: ์ด๋ฏธ์ง€ URL ์บ์‹ฑ (TTL 7์ผ) +- **Fallback 2๋‹จ๊ณ„**: Stable Diffusion ์‹คํŒจ โ†’ DALL-E โ†’ ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ + +--- + +### 2.7 Distribution ์„œ๋น„์Šค (2๊ฐœ) + +| ์‹œ๋‚˜๋ฆฌ์˜ค | ํŒŒ์ผ๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์ฃผ์š” ์ฒ˜๋ฆฌ ๋‚ด์šฉ | +|----------|--------|-----------|---------------| +| ๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ | `distribution-๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ.puml` | UFR-DIST-010 | REST API ๋™๊ธฐ ํ˜ธ์ถœ, ์ฑ„๋„๋ณ„ ๋ณ‘๋ ฌ ๋ฐฐํฌ, DistributionCompleted ๋ฐœํ–‰ | +| ๋ฐฐํฌ์ƒํƒœ์กฐํšŒ | `distribution-๋ฐฐํฌ์ƒํƒœ์กฐํšŒ.puml` | UFR-DIST-020 | ๋ฐฐํฌ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง, ์žฌ์‹œ๋„ ๊ธฐ๋Šฅ | + +**์ฃผ์š” ํŠน์ง•**: +- **๋™๊ธฐ ํ˜ธ์ถœ**: Event Service โ†’ Distribution Service REST API +- **์ฑ„๋„๋ณ„ ๋ณ‘๋ ฌ ๋ฐฐํฌ**: ์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS APIs (par ๋ธ”๋ก) +- **Resilience ํŒจํ„ด**: Circuit Breaker, Retry (3ํšŒ), Bulkhead (์ฑ„๋„๋ณ„ ๋…๋ฆฝ) +- **๋…๋ฆฝ ์ฒ˜๋ฆฌ**: ํ•˜๋‚˜ ์‹คํŒจํ•ด๋„ ๋‹ค๋ฅธ ์ฑ„๋„ ๊ณ„์† +- **Kafka Event**: DistributionCompleted โ†’ Analytics Service ๊ตฌ๋… + +--- + +## 3. ์„ค๊ณ„ ์›์น™ + +### 3.1 ๊ณตํ†ต์„ค๊ณ„์›์น™ ์ค€์ˆ˜ + +โœ… **PlantUML ํ‘œ์ค€** +- `!theme mono` ํ…Œ๋งˆ ์ ์šฉ +- ๋ช…ํ™•ํ•œ ํƒ€์ดํ‹€ ๋ฐ ์ฐธ์—ฌ์ž ํƒ€์ž… ํ‘œ์‹œ +- ์™ธ๋ถ€ ์‹œ์Šคํ…œ/์ธํ”„๋ผ `<>` ํ‘œ์‹œ + +โœ… **๋ ˆ์ด์–ด ์•„ํ‚คํ…์ฒ˜** +``` +Controller (API Layer) + โ†“ +Service (Business Layer) + โ†“ +Repository (Data Layer) + โ†“ +External Systems (Redis, DB, Kafka, APIs) +``` + +โœ… **๋™๊ธฐ/๋น„๋™๊ธฐ ๊ตฌ๋ถ„** +- ์‹ค์„  ํ™”์‚ดํ‘œ (`โ†’`): ๋™๊ธฐ ํ˜ธ์ถœ +- ์ ์„  ํ™”์‚ดํ‘œ (`-->`): ๋น„๋™๊ธฐ ํ˜ธ์ถœ (Kafka) +- `activate`/`deactivate`: ์ƒ๋ช…์„  ํ™œ์„ฑํ™” + +### 3.2 ๋‚ด๋ถ€์‹œํ€€์Šค์„ค๊ณ„ ๊ฐ€์ด๋“œ ์ค€์ˆ˜ + +โœ… **์œ ์ €์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ์„ค๊ณ„** +- 20๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ์™€ ์ •ํ™•ํžˆ ๋งค์นญ +- ๋ถˆํ•„์š”ํ•œ ์ถ”๊ฐ€ ์„ค๊ณ„ ๋ฐฐ์ œ + +โœ… **์™ธ๋ถ€ ์‹œํ€€์Šค์™€ ์ผ์น˜** +- ์™ธ๋ถ€ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ๊ณผ ํ”Œ๋กœ์šฐ ์ผ์น˜ +- ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  ๋ฐฉ์‹ ๋™์ผ + +โœ… **๋ชจ๋“  ๋ ˆ์ด์–ด ํ‘œ์‹œ** +- API, ๋น„์ฆˆ๋‹ˆ์Šค, ๋ฐ์ดํ„ฐ, ์ธํ”„๋ผ ๋ ˆ์ด์–ด ๋ช…์‹œ +- ์บ์‹œ, DB, ์™ธ๋ถ€ API ์ ‘๊ทผ ํ‘œ์‹œ + +--- + +## 4. ์ฃผ์š” ํŒจํ„ด + +### 4.1 Resilience ํŒจํ„ด + +#### Circuit Breaker +- **์ ์šฉ ๋Œ€์ƒ**: ๋ชจ๋“  ์™ธ๋ถ€ API ํ˜ธ์ถœ +- **์„ค์ •**: ์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open, 30์ดˆ ํ›„ Half-Open +- **ํšจ๊ณผ**: ๋น ๋ฅธ ์‹คํŒจ๋กœ ๋ฆฌ์†Œ์Šค ๋ณดํ˜ธ + +#### Retry Pattern +- **์ ์šฉ ๋Œ€์ƒ**: ์ผ์‹œ์  ์žฅ์• ๊ฐ€ ์˜ˆ์ƒ๋˜๋Š” ์™ธ๋ถ€ API +- **์„ค์ •**: ์ตœ๋Œ€ 3ํšŒ, ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ (1์ดˆ, 2์ดˆ, 4์ดˆ) +- **ํšจ๊ณผ**: ์ผ์‹œ์  ์žฅ์•  ์ž๋™ ๋ณต๊ตฌ + +#### Timeout Pattern +- **์ ์šฉ ๋Œ€์ƒ**: ๋ชจ๋“  ์™ธ๋ถ€ API ํ˜ธ์ถœ +- **์„ค์ •**: ๊ตญ์„ธ์ฒญ 5์ดˆ, AI 30์ดˆ, ์ด๋ฏธ์ง€ 20์ดˆ, ๋ฐฐํฌ 10์ดˆ +- **ํšจ๊ณผ**: ๋ฆฌ์†Œ์Šค ์ ์œ  ๋ฐฉ์ง€ + +#### Fallback Pattern +- **์ ์šฉ ๋Œ€์ƒ**: ์™ธ๋ถ€ API ์žฅ์•  ์‹œ +- **์ „๋žต**: ์บ์‹œ๋œ ์ด์ „ ๋ฐ์ดํ„ฐ, ๊ธฐ๋ณธ๊ฐ’, ๊ฒ€์ฆ ์Šคํ‚ต +- **ํšจ๊ณผ**: ์„œ๋น„์Šค ์ง€์†์„ฑ ๋ณด์žฅ (Graceful Degradation) + +#### Bulkhead Pattern +- **์ ์šฉ ๋Œ€์ƒ**: Distribution Service ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ +- **์„ค์ •**: ์ฑ„๋„๋ณ„ ๋…๋ฆฝ ์Šค๋ ˆ๋“œ ํ’€ +- **ํšจ๊ณผ**: ์ฑ„๋„ ์žฅ์•  ๊ฒฉ๋ฆฌ, ์žฅ์•  ์ „ํŒŒ ์ฐจ๋‹จ + +### 4.2 ์บ์‹ฑ ์ „๋žต (Cache-Aside) + +| ์„œ๋น„์Šค | ์บ์‹œ ํ‚ค ํŒจํ„ด | TTL | ํžˆํŠธ์œจ ๋ชฉํ‘œ | ํšจ๊ณผ | +|--------|-------------|-----|-----------|------| +| User | `user:business:{์‚ฌ์—…์ž๋ฒˆํ˜ธ}` | 7์ผ | 90% | 5์ดˆ โ†’ 0.1์ดˆ (98% ๊ฐœ์„ ) | +| AI | `ai:recommendation:{์—…์ข…}:{์ง€์—ญ}:{๋ชฉ์ }` | 24์‹œ๊ฐ„ | 80% | 10์ดˆ โ†’ 0.1์ดˆ (99% ๊ฐœ์„ ) | +| Content | `content:image:{์ด๋ฒคํŠธID}:{์Šคํƒ€์ผ}` | 7์ผ | 80% | 5์ดˆ โ†’ 0.1์ดˆ (98% ๊ฐœ์„ ) | +| Analytics | `analytics:dashboard:{์ด๋ฒคํŠธID}` | 5๋ถ„ | 95% | 3์ดˆ โ†’ 0.5์ดˆ (83% ๊ฐœ์„ ) | +| Event | `event:detail:{eventId}` | 5๋ถ„ | 85% | 1์ดˆ โ†’ 0.2์ดˆ (80% ๊ฐœ์„ ) | +| Participation | `participation:list:{eventId}:{filter}` | 5๋ถ„ | 90% | 2์ดˆ โ†’ 0.3์ดˆ (85% ๊ฐœ์„ ) | + +### 4.3 Event-Driven ํŒจํ„ด + +#### Kafka Event Topics (๋„๋ฉ”์ธ ์ด๋ฒคํŠธ) +- **EventCreated**: ์ด๋ฒคํŠธ ์ƒ์„ฑ ์‹œ โ†’ Analytics Service ๊ตฌ๋… +- **ParticipantRegistered**: ์ฐธ์—ฌ์ž ๋“ฑ๋ก ์‹œ โ†’ Analytics Service ๊ตฌ๋… +- **WinnerSelected**: ๋‹น์ฒจ์ž ์„ ์ • ์‹œ โ†’ (์ถ”ํ›„ ํ™•์žฅ) +- **DistributionCompleted**: ๋ฐฐํฌ ์™„๋ฃŒ ์‹œ โ†’ Analytics Service ๊ตฌ๋… + +#### Kafka Job Topics (๋น„๋™๊ธฐ ์ž‘์—…) +- **ai-job**: AI ์ถ”์ฒœ ์š”์ฒญ โ†’ AI Service ๊ตฌ๋… +- **image-job**: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ โ†’ Content Service ๊ตฌ๋… + +#### ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ +- Redis Set์œผ๋กœ ์ด๋ฒคํŠธ ID ์ค‘๋ณต ์ฒดํฌ +- ๋™์ผ ์ด๋ฒคํŠธ ์ค‘๋ณต ์ฒ˜๋ฆฌ ์‹œ ๋ฌด์‹œ + +--- + +## 5. ํŒŒ์ผ ๊ตฌ์กฐ + +``` +design/backend/sequence/inner/ +โ”œโ”€โ”€ README.md (๋ณธ ๋ฌธ์„œ) +โ”œโ”€โ”€ user-ํšŒ์›๊ฐ€์ž….puml +โ”œโ”€โ”€ user-๋กœ๊ทธ์ธ.puml +โ”œโ”€โ”€ user-ํ”„๋กœํ•„์ˆ˜์ •.puml +โ”œโ”€โ”€ user-๋กœ๊ทธ์•„์›ƒ.puml +โ”œโ”€โ”€ event-๋ชฉ์ ์„ ํƒ.puml +โ”œโ”€โ”€ event-AI์ถ”์ฒœ์š”์ฒญ.puml +โ”œโ”€โ”€ event-์ถ”์ฒœ๊ฒฐ๊ณผ์กฐํšŒ.puml +โ”œโ”€โ”€ event-์ด๋ฏธ์ง€์ƒ์„ฑ์š”์ฒญ.puml +โ”œโ”€โ”€ event-์ด๋ฏธ์ง€๊ฒฐ๊ณผ์กฐํšŒ.puml +โ”œโ”€โ”€ event-์ฝ˜ํ…์ธ ์„ ํƒ.puml +โ”œโ”€โ”€ event-์ตœ์ข…์Šน์ธ๋ฐ๋ฐฐํฌ.puml +โ”œโ”€โ”€ event-์ƒ์„ธ์กฐํšŒ.puml +โ”œโ”€โ”€ event-๋ชฉ๋ก์กฐํšŒ.puml +โ”œโ”€โ”€ event-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml +โ”œโ”€โ”€ participation-์ด๋ฒคํŠธ์ฐธ์—ฌ.puml +โ”œโ”€โ”€ participation-์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ.puml +โ”œโ”€โ”€ participation-๋‹น์ฒจ์ž์ถ”์ฒจ.puml +โ”œโ”€โ”€ analytics-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ-์บ์‹œํžˆํŠธ.puml +โ”œโ”€โ”€ analytics-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ-์บ์‹œ๋ฏธ์Šค.puml +โ”œโ”€โ”€ analytics-์ด๋ฒคํŠธ์ƒ์„ฑ๊ตฌ๋….puml +โ”œโ”€โ”€ analytics-์ฐธ์—ฌ์ž๋“ฑ๋ก๊ตฌ๋….puml +โ”œโ”€โ”€ analytics-๋ฐฐํฌ์™„๋ฃŒ๊ตฌ๋….puml +โ”œโ”€โ”€ ai-ํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ถ”์ฒœ.puml +โ”œโ”€โ”€ content-์ด๋ฏธ์ง€์ƒ์„ฑ.puml +โ”œโ”€โ”€ distribution-๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ.puml +โ””โ”€โ”€ distribution-๋ฐฐํฌ์ƒํƒœ์กฐํšŒ.puml +``` + +**์ด 26๊ฐœ ํŒŒ์ผ, ์•ฝ 114KB** + +--- + +## 6. PlantUML ๋‹ค์ด์–ด๊ทธ๋žจ ํ™•์ธ ๋ฐฉ๋ฒ• + +### 6.1 ์˜จ๋ผ์ธ ํ™•์ธ + +#### PlantUML Web Server +1. https://www.plantuml.com/plantuml/uml ์ ‘์† +2. ๊ฐ `.puml` ํŒŒ์ผ ๋‚ด์šฉ ๋ณต์‚ฌ +3. ์—๋””ํ„ฐ์— ๋ถ™์—ฌ๋„ฃ๊ธฐ +4. ๋‹ค์ด์–ด๊ทธ๋žจ ์‹œ๊ฐ์  ํ™•์ธ +5. PNG/SVG/PDF ๋‹ค์šด๋กœ๋“œ ๊ฐ€๋Šฅ + +#### PlantUML Editor (์ถ”์ฒœ) +1. https://plantuml-editor.kkeisuke.com/ ์ ‘์† +2. ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ œ๊ณต +3. ํŽธ์ง‘ ๋ฐ ๋‹ค์šด๋กœ๋“œ ์ง€์› + +### 6.2 ๋กœ์ปฌ ํ™•์ธ (Docker) + +#### Docker๋กœ PlantUML ๊ฒ€์ฆ +```bash +# Docker ์‹คํ–‰ ํ•„์š” +docker run -d --name plantuml -p 8080:8080 plantuml/plantuml-server:jetty + +# ๊ฐ ํŒŒ์ผ ๋ฌธ๋ฒ• ๊ฒ€์‚ฌ +cat "user-ํšŒ์›๊ฐ€์ž….puml" | docker exec -i plantuml java -jar /app/plantuml.jar -syntax +``` + +### 6.3 IDE ํ”Œ๋Ÿฌ๊ทธ์ธ + +#### IntelliJ IDEA +- **PlantUML Integration** ํ”Œ๋Ÿฌ๊ทธ์ธ ์„ค์น˜ +- `.puml` ํŒŒ์ผ ์šฐํด๋ฆญ โ†’ "Show PlantUML Diagram" + +#### VS Code +- **PlantUML** ํ™•์žฅ ์„ค์น˜ +- `Alt+D`: ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์—ด๊ธฐ + +--- + +## ๋ถ€๋ก + +### A. ํŒŒ์ผ ํฌ๊ธฐ ๋ฐ ํ†ต๊ณ„ + +| ์„œ๋น„์Šค | ์‹œ๋‚˜๋ฆฌ์˜ค ์ˆ˜ | ์ด ํฌ๊ธฐ | ํ‰๊ท  ํฌ๊ธฐ | +|--------|------------|---------|----------| +| User | 4 | 21.2KB | 5.3KB | +| Event | 10 | 20.2KB | 2.0KB | +| Participation | 3 | 15.4KB | 5.1KB | +| Analytics | 5 | 20.8KB | 4.2KB | +| AI | 1 | 12KB | 12KB | +| Content | 1 | 8.5KB | 8.5KB | +| Distribution | 2 | 17.5KB | 8.8KB | +| **์ด๊ณ„** | **26** | **115.6KB** | **4.4KB** | + +### B. ์ฃผ์š” ๊ธฐ์ˆ  ์Šคํƒ + +#### Backend +- **Framework**: Spring Boot +- **ORM**: JPA/Hibernate +- **Security**: Spring Security + JWT +- **Cache**: Redis +- **Database**: PostgreSQL +- **Message Queue**: Apache Kafka + +#### Resilience +- **Circuit Breaker**: Resilience4j +- **Retry**: Resilience4j RetryRegistry +- **Timeout**: Resilience4j TimeLimiterRegistry + +#### Utilities +- **Password**: bcrypt (Spring Security) +- **JWT**: jjwt library +- **Encryption**: AES-256 (javax.crypto) + +### C. ์ฐธ๊ณ  ๋ฌธ์„œ +- [์œ ์ €์Šคํ† ๋ฆฌ](../../userstory.md) +- [์™ธ๋ถ€ ์‹œํ€€์Šค ์„ค๊ณ„์„œ](../outer/) +- [๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜](../../logical/logical-architecture.md) +- [๊ณตํ†ต์„ค๊ณ„์›์น™](../../../../claude/common-principles.md) +- [๋‚ด๋ถ€์‹œํ€€์Šค์„ค๊ณ„ ๊ฐ€์ด๋“œ](../../../../claude/sequence-inner-design.md) + +--- + +**๋ฌธ์„œ ๋ฒ„์ „**: 1.0 +**์ตœ์ข… ์ˆ˜์ •์ผ**: 2025-10-22 +**์ž‘์„ฑ์ž**: System Architect (๋ฐ•์˜์ž) +**๋‚ด๋ถ€ ์‹œํ€€์Šค ์„ค๊ณ„ ์™„๋ฃŒ**: โœ… 26๊ฐœ ์‹œ๋‚˜๋ฆฌ์˜ค ๋ชจ๋‘ ์ž‘์„ฑ ์™„๋ฃŒ diff --git a/design/backend/sequence/inner/ai-ํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ถ”์ฒœ.puml b/design/backend/sequence/inner/ai-ํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ถ”์ฒœ.puml new file mode 100644 index 0000000..4cd369a --- /dev/null +++ b/design/backend/sequence/inner/ai-ํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ถ”์ฒœ.puml @@ -0,0 +1,343 @@ +@startuml ai-ํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ถ”์ฒœ +!theme mono + +title AI Service - ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ (๋‚ด๋ถ€ ์‹œํ€€์Šค) + +actor Client +participant "Kafka Consumer" as Consumer <> +participant "JobMessageHandler" as Handler <> +participant "AIRecommendationService" as Service <> +participant "TrendAnalysisEngine" as TrendEngine <> +participant "RecommendationEngine" as RecommendEngine <> +participant "CacheManager" as Cache <> +participant "CircuitBreakerManager" as CB <> +participant "ExternalAIClient" as AIClient <> +participant "JobStateManager" as JobState <> +participant "Redis" as Redis <> +participant "External AI API" as ExternalAPI <> +participant "Kafka Producer" as Producer <> + +note over Consumer: Kafka ai-job Topic ๊ตฌ๋…\nConsumer Group: ai-service-group + +== 1. Job ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  == +Consumer -> Handler: onMessage(jobMessage)\n{jobId, eventDraftId, ๋ชฉ์ , ์—…์ข…, ์ง€์—ญ, ๋งค์žฅ์ •๋ณด} +activate Handler + +Handler -> Handler: ๋ฉ”์‹œ์ง€ ์œ ํšจ์„ฑ ๊ฒ€์ฆ +note right +๊ฒ€์ฆ ํ•ญ๋ชฉ: +- jobId ์กด์žฌ ์—ฌ๋ถ€ +- eventDraftId ์œ ํšจ์„ฑ +- ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ (๋ชฉ์ , ์—…์ข…, ์ง€์—ญ) +end note + +alt ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฉ”์‹œ์ง€ + Handler -> Producer: DLQ ๋ฐœํ–‰ (Dead Letter Queue)\n{jobId, error: INVALID_MESSAGE} + Handler --> Consumer: ACK (๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์™„๋ฃŒ) + note over Handler: ์ž˜๋ชป๋œ ๋ฉ”์‹œ์ง€๋Š” DLQ๋กœ ์ด๋™\n์ˆ˜๋™ ๊ฒ€ํ†  ํ•„์š” +else ์œ ํšจํ•œ ๋ฉ”์‹œ์ง€ + Handler -> JobState: updateJobStatus(jobId, PROCESSING) + JobState -> Redis: SET job:{jobId}:status = PROCESSING + Redis --> JobState: OK + JobState --> Handler: ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ + + Handler -> Service: generateRecommendations(\neventDraftId, ๋ชฉ์ , ์—…์ข…, ์ง€์—ญ, ๋งค์žฅ์ •๋ณด) + activate Service + + == 2. ํŠธ๋ Œ๋“œ ๋ถ„์„ == + Service -> TrendEngine: analyzeTrends(์—…์ข…, ์ง€์—ญ, ๋ชฉ์ ) + activate TrendEngine + + TrendEngine -> Cache: getCachedTrend(์—…์ข…, ์ง€์—ญ) + Cache -> Redis: GET trend:{์—…์ข…}:{์ง€์—ญ} + Redis --> Cache: ์บ์‹œ ๊ฒฐ๊ณผ + + alt ์บ์‹œ ํžˆํŠธ + Cache --> TrendEngine: ์บ์‹œ๋œ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ + note right + ์บ์‹œ ํ‚ค: trend:{์—…์ข…}:{์ง€์—ญ} + TTL: 1์‹œ๊ฐ„ + ๋ฐ์ดํ„ฐ: { + industry_trends, + regional_characteristics, + seasonal_patterns + } + end note + + else ์บ์‹œ ๋ฏธ์Šค + note right of TrendEngine + **ํŠธ๋ Œ๋“œ ๋ถ„์„ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ** + - ์—…์ข… ์ •๋ณด + - ์ง€์—ญ ์ •๋ณด + - ํ˜„์žฌ ์‹œ์ฆŒ (๊ณ„์ ˆ, ์›”) + - ์ด๋ฒคํŠธ ๋ชฉ์  + + **์™ธ๋ถ€ AI API ํ˜ธ์ถœ** + - ๊ณผ๊ฑฐ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ ์•ˆ ํ•จ + - ์‹ค์‹œ๊ฐ„ ์‹œ์žฅ ํŠธ๋ Œ๋“œ ๋ถ„์„ + - ์—…์ข…๋ณ„/์ง€์—ญ๋ณ„ ์ผ๋ฐ˜์  ํŠน์„ฑ + end note + + TrendEngine -> CB: executeWithCircuitBreaker(\nAI API ํŠธ๋ Œ๋“œ ๋ถ„์„ ํ˜ธ์ถœ) + activate CB + + CB -> CB: Circuit Breaker ์ƒํƒœ ํ™•์ธ + note right + **Circuit Breaker ์„ค์ •** + - Failure Rate Threshold: 50% + - Timeout: 5๋ถ„ (300์ดˆ) + - Half-Open Wait Duration: 1๋ถ„ (60์ดˆ) + - Permitted Calls in Half-Open: 3 + - Sliding Window Size: 10 + end note + + alt Circuit CLOSED (์ •์ƒ) + CB -> AIClient: callAIAPI(\nmethod: "trendAnalysis",\nprompt: ํŠธ๋ Œ๋“œ ๋ถ„์„ ํ”„๋กฌํ”„ํŠธ,\ntimeout: 5๋ถ„) + activate AIClient + + AIClient -> AIClient: ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ + note right of AIClient + **AI ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ** + "๋‹น์‹ ์€ ๋งˆ์ผ€ํŒ… ํŠธ๋ Œ๋“œ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. + + **์ž…๋ ฅ ์ •๋ณด** + - ์—…์ข…: {์—…์ข…} + - ์ง€์—ญ: {์ง€์—ญ} + - ํ˜„์žฌ ์‹œ์ฆŒ: {๊ณ„์ ˆ/์›”} + - ์ด๋ฒคํŠธ ๋ชฉ์ : {๋ชฉ์ } + + **๋ถ„์„ ์š”์ฒญ์‚ฌํ•ญ** + 1. ์—…์ข…๋ณ„ ์ผ๋ฐ˜์  ํŠธ๋ Œ๋“œ + (์—…์ข… ํŠน์„ฑ ๊ธฐ๋ฐ˜ ํšจ๊ณผ์ ์ธ ์ด๋ฒคํŠธ ์œ ํ˜•) + + 2. ์ง€์—ญ๋ณ„ ํŠน์„ฑ + (์ง€์—ญ ๊ณ ๊ฐ ํŠน์„ฑ, ์„ ํ˜ธ๋„) + + 3. ์‹œ์ฆŒ๋ณ„ ์ถ”์ฒœ + (ํ˜„์žฌ ์‹œ๊ธฐ์— ์ ํ•ฉํ•œ ์ด๋ฒคํŠธ)" + end note + + AIClient -> ExternalAPI: AI API ํ˜ธ์ถœ\nPOST /api/v1/analyze\nAuthorization: Bearer {API_KEY}\nTimeout: 5๋ถ„\nPayload: {์—…์ข…, ์ง€์—ญ, ์‹œ์ฆŒ, ๋ชฉ์ } + activate ExternalAPI + + ExternalAPI --> AIClient: 200 OK\n{"industryTrend": "...",\n"regionalCharacteristics": "...",\n"seasonalRecommendation": "..."} + deactivate ExternalAPI + + AIClient -> AIClient: ์‘๋‹ต ๊ฒ€์ฆ ๋ฐ ํŒŒ์‹ฑ + AIClient --> CB: ๋ถ„์„ ๊ฒฐ๊ณผ + deactivate AIClient + + CB -> CB: ์„ฑ๊ณต ๊ธฐ๋ก (Circuit Breaker) + CB --> TrendEngine: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ + deactivate CB + + TrendEngine -> Cache: cacheTrend(\nkey: trend:{์—…์ข…}:{์ง€์—ญ},\ndata: ๋ถ„์„๊ฒฐ๊ณผ,\nTTL: 1์‹œ๊ฐ„) + Cache -> Redis: SETEX trend:{์—…์ข…}:{์ง€์—ญ} 3600 {๋ถ„์„๊ฒฐ๊ณผ} + Redis --> Cache: OK + Cache --> TrendEngine: ์บ์‹ฑ ์™„๋ฃŒ + + else Circuit OPEN (์žฅ์• ) + CB --> TrendEngine: CircuitBreakerOpenException + TrendEngine -> TrendEngine: Fallback ์‹คํ–‰\n(๊ธฐ๋ณธ ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ) + note right + Fallback ์ „๋žต: + - ์ด์ „ ์บ์‹œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + - ๋˜๋Š” ๊ธฐ๋ณธ ํŠธ๋ Œ๋“œ ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ + - ํด๋ผ์ด์–ธํŠธ์— ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ํฌํ•จ + end note + + else Circuit HALF-OPEN (๋ณต๊ตฌ ์‹œ๋„) + CB -> AIClient: ์ œํ•œ๋œ ์š”์ฒญ ํ—ˆ์šฉ (3๊ฐœ) + AIClient -> ExternalAPI: POST /api/v1/analyze + ExternalAPI --> AIClient: 200 OK + AIClient --> CB: ์„ฑ๊ณต + CB -> CB: ์—ฐ์† ์„ฑ๊ณต ์‹œ CLOSED๋กœ ์ „ํ™˜ + CB --> TrendEngine: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ + + else Timeout (5๋ถ„ ์ดˆ๊ณผ) + CB --> TrendEngine: TimeoutException + note right of TrendEngine + **Timeout ์ฒ˜๋ฆฌ** + - 5๋ถ„ ์ดˆ๊ณผ ์‹œ ์ฆ‰์‹œ ์‹คํŒจ + - Fallback: ๊ธฐ๋ณธ ํŠธ๋ Œ๋“œ ์‚ฌ์šฉ + - ์‚ฌ์šฉ์ž์—๊ฒŒ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ์ œ๊ณต + end note + TrendEngine -> TrendEngine: Fallback ์‹คํ–‰\n(๊ธฐ๋ณธ ํŠธ๋ Œ๋“œ ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ) + end + end + + TrendEngine --> Service: ํŠธ๋ Œ๋“œ ๋ถ„์„ ์™„๋ฃŒ\n{์—…์ข…ํŠธ๋ Œ๋“œ, ์ง€์—ญํŠน์„ฑ, ์‹œ์ฆŒํŠน์„ฑ} + deactivate TrendEngine + + == 3. ์ด๋ฒคํŠธ ์ถ”์ฒœ ์ƒ์„ฑ (3๊ฐ€์ง€ ์˜ต์…˜) == + Service -> RecommendEngine: generateRecommendations(\n๋ชฉ์ , ํŠธ๋ Œ๋“œ, ๋งค์žฅ์ •๋ณด) + activate RecommendEngine + + RecommendEngine -> RecommendEngine: ์ถ”์ฒœ ์ปจํ…์ŠคํŠธ ๊ตฌ์„ฑ + note right + ์ถ”์ฒœ ์ž…๋ ฅ: + - ์ด๋ฒคํŠธ ๋ชฉ์  (์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜ ๋“ฑ) + - ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ + - ๋งค์žฅ ์ •๋ณด (์—…์ข…, ์œ„์น˜, ํฌ๊ธฐ) + - ์˜ˆ์‚ฐ ๋ฒ”์œ„ (์ €/์ค‘/๊ณ ) + end note + + group parallel + RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API ์ถ”์ฒœ ์ƒ์„ฑ - ์˜ต์…˜ 1: ์ €๋น„์šฉ) + activate CB + CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: ์ €๋น„์šฉ ์ถ”์ฒœ ํ”„๋กฌํ”„ํŠธ,\ntimeout: 5๋ถ„) + activate AIClient + + AIClient -> AIClient: ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ + note right + ์˜ต์…˜ 1 ํ”„๋กฌํ”„ํŠธ: + "์ €๋น„์šฉ, ๋†’์€ ์ฐธ์—ฌ์œจ ์ค‘์‹ฌ ์ด๋ฒคํŠธ ๊ธฐํš + ๋ชฉ์ : {๋ชฉ์ } + ํŠธ๋ Œ๋“œ: {ํŠธ๋ Œ๋“œ} + ๋งค์žฅ: {๋งค์žฅ์ •๋ณด} + + ์ถœ๋ ฅ ํ˜•์‹: + - ์ด๋ฒคํŠธ ์ œ๋ชฉ + - ์ถ”์ฒœ ๊ฒฝํ’ˆ (์˜ˆ์‚ฐ: ์ €) + - ์ฐธ์—ฌ ๋ฐฉ๋ฒ• (๋‚œ์ด๋„: ๋‚ฎ์Œ) + - ์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜ + - ์˜ˆ์ƒ ๋น„์šฉ + - ์˜ˆ์ƒ ROI" + end note + + AIClient -> ExternalAPI: POST /api/v1/recommend\n(์ €๋น„์šฉ ์˜ต์…˜) + ExternalAPI --> AIClient: 200 OK\n{์ถ”์ฒœ์•ˆ 1} + AIClient --> CB: ์ถ”์ฒœ์•ˆ 1 + deactivate AIClient + CB --> RecommendEngine: ์˜ต์…˜ 1 ์™„๋ฃŒ + deactivate CB + + RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API ์ถ”์ฒœ ์ƒ์„ฑ - ์˜ต์…˜ 2: ์ค‘๋น„์šฉ) + activate CB + CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: ์ค‘๋น„์šฉ ์ถ”์ฒœ ํ”„๋กฌํ”„ํŠธ,\ntimeout: 5๋ถ„) + activate AIClient + + AIClient -> AIClient: ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ + note right + ์˜ต์…˜ 2 ํ”„๋กฌํ”„ํŠธ: + "์ค‘๋น„์šฉ, ๊ท ํ˜•์žกํžŒ ROI ์ด๋ฒคํŠธ ๊ธฐํš + ๋ชฉ์ : {๋ชฉ์ } + ํŠธ๋ Œ๋“œ: {ํŠธ๋ Œ๋“œ} + ๋งค์žฅ: {๋งค์žฅ์ •๋ณด} + + ์ถœ๋ ฅ ํ˜•์‹: + - ์ด๋ฒคํŠธ ์ œ๋ชฉ + - ์ถ”์ฒœ ๊ฒฝํ’ˆ (์˜ˆ์‚ฐ: ์ค‘) + - ์ฐธ์—ฌ ๋ฐฉ๋ฒ• (๋‚œ์ด๋„: ์ค‘๊ฐ„) + - ์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜ + - ์˜ˆ์ƒ ๋น„์šฉ + - ์˜ˆ์ƒ ROI" + end note + + AIClient -> ExternalAPI: POST /api/v1/recommend\n(์ค‘๋น„์šฉ ์˜ต์…˜) + ExternalAPI --> AIClient: 200 OK\n{์ถ”์ฒœ์•ˆ 2} + AIClient --> CB: ์ถ”์ฒœ์•ˆ 2 + deactivate AIClient + CB --> RecommendEngine: ์˜ต์…˜ 2 ์™„๋ฃŒ + deactivate CB + + RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API ์ถ”์ฒœ ์ƒ์„ฑ - ์˜ต์…˜ 3: ๊ณ ๋น„์šฉ) + activate CB + CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: ๊ณ ๋น„์šฉ ์ถ”์ฒœ ํ”„๋กฌํ”„ํŠธ,\ntimeout: 5๋ถ„) + activate AIClient + + AIClient -> AIClient: ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ + note right + ์˜ต์…˜ 3 ํ”„๋กฌํ”„ํŠธ: + "๊ณ ๋น„์šฉ, ๋†’์€ ๋งค์ถœ ์ฆ๋Œ€ ์ด๋ฒคํŠธ ๊ธฐํš + ๋ชฉ์ : {๋ชฉ์ } + ํŠธ๋ Œ๋“œ: {ํŠธ๋ Œ๋“œ} + ๋งค์žฅ: {๋งค์žฅ์ •๋ณด} + + ์ถœ๋ ฅ ํ˜•์‹: + - ์ด๋ฒคํŠธ ์ œ๋ชฉ + - ์ถ”์ฒœ ๊ฒฝํ’ˆ (์˜ˆ์‚ฐ: ๊ณ ) + - ์ฐธ์—ฌ ๋ฐฉ๋ฒ• (๋‚œ์ด๋„: ๋†’์Œ) + - ์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜ + - ์˜ˆ์ƒ ๋น„์šฉ + - ์˜ˆ์ƒ ROI" + end note + + AIClient -> ExternalAPI: POST /api/v1/recommend\n(๊ณ ๋น„์šฉ ์˜ต์…˜) + ExternalAPI --> AIClient: 200 OK\n{์ถ”์ฒœ์•ˆ 3} + AIClient --> CB: ์ถ”์ฒœ์•ˆ 3 + deactivate AIClient + CB --> RecommendEngine: ์˜ต์…˜ 3 ์™„๋ฃŒ + deactivate CB + end + + RecommendEngine -> RecommendEngine: 3๊ฐ€์ง€ ์ถ”์ฒœ์•ˆ ํ†ตํ•ฉ ๋ฐ ๊ฒ€์ฆ + note right + ๊ฒ€์ฆ ํ•ญ๋ชฉ: + - ํ•„์ˆ˜ ํ•„๋“œ ์กด์žฌ ์—ฌ๋ถ€ + - ์˜ˆ์ƒ ์„ฑ๊ณผ ๊ณ„์‚ฐ (ROI) + - ์ถ”์ฒœ์•ˆ ์ฐจ๋ณ„ํ™” ํ™•์ธ + - ํ™๋ณด ๋ฌธ๊ตฌ ์ƒ์„ฑ (๊ฐ 5๊ฐœ) + - SNS ํ•ด์‹œํƒœ๊ทธ ์ž๋™ ์ƒ์„ฑ + end note + + RecommendEngine --> Service: 3๊ฐ€์ง€ ์ถ”์ฒœ์•ˆ ์ƒ์„ฑ ์™„๋ฃŒ + deactivate RecommendEngine + + == 4. ๊ฒฐ๊ณผ ์ €์žฅ ๋ฐ Job ์ƒํƒœ ์—…๋ฐ์ดํŠธ == + Service -> Cache: cacheRecommendations(\nkey: ai:recommendation:{eventDraftId},\ndata: {ํŠธ๋ Œ๋“œ+์ถ”์ฒœ์•ˆ},\nTTL: 24์‹œ๊ฐ„) + Cache -> Redis: SETEX ai:recommendation:{eventDraftId} 86400 {๊ฒฐ๊ณผ} + Redis --> Cache: OK + Cache --> Service: ์บ์‹ฑ ์™„๋ฃŒ + + Service -> JobState: updateJobStatus(\njobId,\nstatus: COMPLETED,\nresult: {ํŠธ๋ Œ๋“œ, ์ถ”์ฒœ์•ˆ}) + JobState -> Redis: HSET job:{jobId} status COMPLETED result {JSON} + Redis --> JobState: OK + JobState --> Service: ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ + + Service --> Handler: ์ถ”์ฒœ ์ƒ์„ฑ ์™„๋ฃŒ\n{ํŠธ๋ Œ๋“œ๋ถ„์„, 3๊ฐ€์ง€์ถ”์ฒœ์•ˆ} + deactivate Service + + Handler --> Consumer: ACK (๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์™„๋ฃŒ) + deactivate Handler + + note over Consumer: Job ์ฒ˜๋ฆฌ ์™„๋ฃŒ\nRedis์— ์ €์žฅ๋œ ๊ฒฐ๊ณผ๋ฅผ\nํด๋ผ์ด์–ธํŠธ๋Š” ํด๋ง์œผ๋กœ ์กฐํšŒ +end + +== ์˜ˆ์™ธ ์ฒ˜๋ฆฌ == +note over Handler, Producer +**AI API ์žฅ์•  ์‹œ** +- Circuit Breaker Open +- Fallback: ๊ธฐ๋ณธ ํŠธ๋ Œ๋“œ ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ +- Job ์ƒํƒœ: COMPLETED (์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ํฌํ•จ) +- ์‚ฌ์šฉ์ž์—๊ฒŒ "AI ๋ถ„์„์ด ์ œํ•œ์ ์œผ๋กœ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค" ์•ˆ๋‚ด + +**Timeout (5๋ถ„ ์ดˆ๊ณผ)** +- Circuit Breaker๋กœ ์ฆ‰์‹œ ์‹คํŒจ +- Retry ์—†์Œ (๋น„๋™๊ธฐ Job) +- Job ์ƒํƒœ: FAILED +- ์‚ฌ์šฉ์ž์—๊ฒŒ ์žฌ์‹œ๋„ ์š”์ฒญ ์•ˆ๋‚ด + +**Kafka ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์‹คํŒจ** +- DLQ(Dead Letter Queue)๋กœ ์ด๋™ +- ์ˆ˜๋™ ๊ฒ€ํ†  ๋ฐ ์žฌ์ฒ˜๋ฆฌ +- ์—๋Ÿฌ ๋กœ๊ทธ ๊ธฐ๋ก + +**Redis ์žฅ์• ** +- ์บ์‹ฑ ์Šคํ‚ต +- Job ์ƒํƒœ๋Š” ๋ฉ”๋ชจ๋ฆฌ์— ์ž„์‹œ ์ €์žฅ +- ์„ฑ๋Šฅ ์ €ํ•˜ ๊ฐ€๋Šฅ (๋งค ์š”์ฒญ๋งˆ๋‹ค AI API ํ˜ธ์ถœ) + +**์„ฑ๋Šฅ ๋ชฉํ‘œ** +- ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„: 2๋ถ„ ์ด๋‚ด +- P95 ์‘๋‹ต ์‹œ๊ฐ„: 4๋ถ„ ์ด๋‚ด +- Circuit Breaker Timeout: 5๋ถ„ +- Redis ์บ์‹œ TTL: 24์‹œ๊ฐ„ + +**๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์›์น™** +- ๊ณผ๊ฑฐ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ ์•ˆ ํ•จ +- ์™ธ๋ถ€ AI API๋กœ ์‹ค์‹œ๊ฐ„ ํŠธ๋ Œ๋“œ ๋ถ„์„ +- ์—…์ข…/์ง€์—ญ ๊ธฐ๋ฐ˜ ์ผ๋ฐ˜์  ๋งˆ์ผ€ํŒ… ํŠธ๋ Œ๋“œ ํ™œ์šฉ +end note + +@enduml diff --git a/design/backend/sequence/inner/analytics-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml b/design/backend/sequence/inner/analytics-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml new file mode 100644 index 0000000..785eaa7 --- /dev/null +++ b/design/backend/sequence/inner/analytics-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml @@ -0,0 +1,342 @@ +@startuml analytics-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ +!theme mono + +title Analytics Service - ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ ๋‚ด๋ถ€ ์‹œํ€€์Šค\n(UFR-ANAL-010: ์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ) + +participant "AnalyticsController" as Controller +participant "AnalyticsService" as Service +participant "CacheService" as Cache +participant "AnalyticsRepository" as Repository +participant "ExternalChannelService" as ChannelService +participant "ROICalculator" as Calculator +participant "CircuitBreaker" as CB +participant "Redis<>" as Redis +database "Analytics DB<>" as DB + +-> Controller: GET /api/events/{id}/analytics\n+ Authorization: Bearer {token} +activate Controller + +Controller -> Service: getDashboardData(eventId, userId) +activate Service + +note right of Service + **์ž…๋ ฅ ๊ฒ€์ฆ** + - eventId: UUID ํ˜•์‹ ๊ฒ€์ฆ + - userId: JWT์—์„œ ์ถ”์ถœ + - ๊ถŒํ•œ ํ™•์ธ: ๋งค์žฅ ์†Œ์œ ์ž ์—ฌ๋ถ€ +end note + +Service -> Cache: get("analytics:dashboard:{eventId}") +activate Cache + +note right of Cache + **Cache-Aside ํŒจํ„ด** + - Redis GET ํ˜ธ์ถœ + - Cache Key ๊ตฌ์กฐ: + analytics:dashboard:{eventId} + - TTL: 3600์ดˆ (1์‹œ๊ฐ„) +end note + +Cache -> Redis: GET analytics:dashboard:{eventId} +activate Redis + +alt Cache HIT + + Redis --> Cache: **Cache HIT**\n์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜\n{\n totalParticipants: 1234,\n totalViews: 17200,\n roi: 250,\n channelStats: [...],\n lastUpdated: "2025-10-22T10:30:00Z"\n} + deactivate Redis + + Cache --> Service: Dashboard ๋ฐ์ดํ„ฐ (JSON) + deactivate Cache + + note right of Service + **์‘๋‹ต ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ** + - 4๊ฐœ ์š”์•ฝ ์นด๋“œ + * ์ด ์ฐธ์—ฌ์ž ์ˆ˜, ๋‹ฌ์„ฑ๋ฅ  + * ์ด ๋…ธ์ถœ ์ˆ˜, ์ฆ๊ฐ๋ฅ  + * ์˜ˆ์ƒ ROI, ์—…์ข… ํ‰๊ท  ๋Œ€๋น„ + * ๋งค์ถœ ์ฆ๊ฐ€์œจ + - ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ + - ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด + - ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ถ„์„ + - ๋น„๊ต ๋ถ„์„ (์—…์ข… ํ‰๊ท , ์ด์ „ ์ด๋ฒคํŠธ) + end note + + Service --> Controller: DashboardResponse\n(200 OK) + deactivate Service + + Controller --> : 200 OK\nDashboard Data (JSON) + deactivate Controller + + note over Controller, Redis + **Cache HIT ์‹œ๋‚˜๋ฆฌ์˜ค ์„ฑ๋Šฅ** + - ์‘๋‹ต ์‹œ๊ฐ„: ์•ฝ 0.5์ดˆ + - Redis ์กฐํšŒ ์‹œ๊ฐ„: 0.01์ดˆ + - ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”: 0.05์ดˆ + - HTTP ์˜ค๋ฒ„ํ—ค๋“œ: 0.44์ดˆ + - ์˜ˆ์ƒ ํžˆํŠธ์œจ: 95% + end note + +else Cache MISS + + Redis --> Cache: **Cache MISS** (null) + deactivate Redis + + Cache --> Service: null (์บ์‹œ ๋ฏธ์Šค) + deactivate Cache + + note right of Service + **Cache MISS ์ฒ˜๋ฆฌ** + - ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ์ž‘์—… ์‹œ์ž‘ + - ๋กœ์ปฌ DB ์กฐํšŒ + ์™ธ๋ถ€ API ๋ณ‘๋ ฌ ํ˜ธ์ถœ + end note + + ||| + == 1. Analytics DB ์กฐํšŒ (๋กœ์ปฌ ๋ฐ์ดํ„ฐ) == + + Service -> Repository: getEventStats(eventId) + activate Repository + + Repository -> DB: ์ด๋ฒคํŠธ ํ†ต๊ณ„ ์กฐํšŒ\n(์ด๋ฒคํŠธID๋กœ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ) + activate DB + + DB --> Repository: EventStatsEntity\n- totalParticipants\n- estimatedROI\n- salesGrowthRate + deactivate DB + + Repository --> Service: EventStats + deactivate Repository + + note right of Service + **๋กœ์ปฌ ๋ฐ์ดํ„ฐ ํ™•๋ณด** + - ์ด ์ฐธ์—ฌ์ž ์ˆ˜ + - ์˜ˆ์ƒ ROI (DB ์บ์‹œ) + - ๋งค์ถœ ์ฆ๊ฐ€์œจ (POS ์—ฐ๋™) + end note + + ||| + == 2. ์™ธ๋ถ€ ์ฑ„๋„ API ๋ณ‘๋ ฌ ํ˜ธ์ถœ (Circuit Breaker ์ ์šฉ) == + + note right of Service + **๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ์‹œ์ž‘** + - CompletableFuture 4๊ฐœ ์ƒ์„ฑ + - ์šฐ๋ฆฌ๋™๋„คTV, ์ง€๋‹ˆTV, ๋ง๊ณ ๋น„์ฆˆ, SNS APIs ๋™์‹œ ํ˜ธ์ถœ + - Circuit Breaker ์ ์šฉ (์ฑ„๋„๋ณ„ ๋…๋ฆฝ) + end note + + par ์™ธ๋ถ€ API ๋ณ‘๋ ฌ ํ˜ธ์ถœ + + Service -> ChannelService: getWooriTVStats(eventId) + activate ChannelService + + ChannelService -> CB: execute("wooriTV", () -> callAPI()) + activate CB + + note right of CB + **Circuit Breaker** + - State: CLOSED (์ •์ƒ) + - Failure Rate: 50% ์ดˆ๊ณผ ์‹œ OPEN + - Timeout: 10์ดˆ + end note + + CB -> CB: ์™ธ๋ถ€ API ํ˜ธ์ถœ\nGET /stats/{eventId} + + alt Circuit Breaker CLOSED (์ •์ƒ) + CB --> ChannelService: ChannelStats\n- views: 5000\n- clicks: 1200 + deactivate CB + + ChannelService --> Service: WooriTVStats + deactivate ChannelService + else Circuit Breaker OPEN (์žฅ์• ) + CB -> CB: **Fallback ์‹คํ–‰**\n์บ์‹œ๋œ ์ด์ „ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + note right of CB + Fallback ์ „๋žต: + - Redis์—์„œ ์ด์ „ ํ†ต๊ณ„ ์กฐํšŒ + - ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ (0) ๋ฐ˜ํ™˜ + - ์•Œ๋ฆผ: "์ผ๋ถ€ ์ฑ„๋„ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ" + end note + CB --> ChannelService: Fallback ๋ฐ์ดํ„ฐ + deactivate CB + ChannelService --> Service: WooriTVStats (Fallback) + deactivate ChannelService + end + + else + + Service -> ChannelService: getGenieTVStats(eventId) + activate ChannelService + + ChannelService -> CB: execute("genieTV", () -> callAPI()) + activate CB + + CB -> CB: ์™ธ๋ถ€ API ํ˜ธ์ถœ\nGET /campaign/{id}/stats + + alt ์ •์ƒ ์‘๋‹ต + CB --> ChannelService: ChannelStats\n- adViews: 10000\n- clicks: 500 + deactivate CB + ChannelService --> Service: GenieTVStats + deactivate ChannelService + else Timeout (10์ดˆ ์ดˆ๊ณผ) + CB -> CB: **Timeout ์ฒ˜๋ฆฌ**\n๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ + note right of CB + Timeout ๋ฐœ์ƒ: + - ๋ฆฌ์†Œ์Šค ์ ์œ  ๋ฐฉ์ง€ + - Fallback์œผ๋กœ ๊ธฐ๋ณธ๊ฐ’ (0) ์„ค์ • + - ์•Œ๋ฆผ: "์ง€๋‹ˆTV ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์ง€์—ฐ" + end note + CB --> ChannelService: ๊ธฐ๋ณธ๊ฐ’ (0) + deactivate CB + ChannelService --> Service: GenieTVStats (๊ธฐ๋ณธ๊ฐ’) + deactivate ChannelService + end + + else + + Service -> ChannelService: getRingoBizStats(eventId) + activate ChannelService + + ChannelService -> CB: execute("ringoBiz", () -> callAPI()) + activate CB + + note right of CB + **Circuit Breaker** + - State: CLOSED (์ •์ƒ) + - Failure Rate: 50% ์ดˆ๊ณผ ์‹œ OPEN + - Timeout: 10์ดˆ + end note + + CB -> CB: ์™ธ๋ถ€ API ํ˜ธ์ถœ\nGET /voice-stats/{eventId} + + alt ์ •์ƒ ์‘๋‹ต + CB --> ChannelService: ChannelStats\n- calls: 3000\n- completed: 2500\n- avgDuration: 45์ดˆ + deactivate CB + ChannelService --> Service: RingoBizStats + deactivate ChannelService + else Timeout ๋˜๋Š” ์žฅ์•  + CB -> CB: **Fallback ์‹คํ–‰**\n๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ + note right of CB + ๋ง๊ณ ๋น„์ฆˆ API ์žฅ์• : + - ๊ธฐ๋ณธ๊ฐ’ (0) ๋ฐ˜ํ™˜ + - ์•Œ๋ฆผ: "๋ง๊ณ ๋น„์ฆˆ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ" + end note + CB --> ChannelService: ๊ธฐ๋ณธ๊ฐ’ (0) + deactivate CB + ChannelService --> Service: RingoBizStats (๊ธฐ๋ณธ๊ฐ’) + deactivate ChannelService + end + + else + + Service -> ChannelService: getSNSStats(eventId) + activate ChannelService + + ChannelService -> CB: execute("SNS", () -> callAPIs()) + activate CB + + note right of CB + **SNS APIs ํ†ตํ•ฉ ํ˜ธ์ถœ** + - Instagram API + - Naver Blog API + - Kakao Channel API + - 3๊ฐœ API ๋ณ‘๋ ฌ ํ˜ธ์ถœ + end note + + CB -> CB: ์™ธ๋ถ€ APIs ํ˜ธ์ถœ\n(Instagram, Naver, Kakao) + + alt ์ •์ƒ ์‘๋‹ต + CB --> ChannelService: SNSStats\n- Instagram: likes 300, comments 50\n- Naver: views 2000\n- Kakao: shares 100 + deactivate CB + ChannelService --> Service: SNSStats + deactivate ChannelService + else ์žฅ์•  ๋˜๋Š” Timeout + CB -> CB: **Fallback ์‹คํ–‰**\n๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜ + note right of CB + SNS API ์žฅ์• : + - ๊ธฐ๋ณธ๊ฐ’ (0) ๋ฐ˜ํ™˜ + - ์•Œ๋ฆผ: "SNS ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ" + end note + CB --> ChannelService: ๊ธฐ๋ณธ๊ฐ’ (0) + deactivate CB + ChannelService --> Service: SNSStats (๊ธฐ๋ณธ๊ฐ’) + deactivate ChannelService + end + + end + + ||| + == 3. ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ๋ฐ ROI ๊ณ„์‚ฐ == + + Service -> Service: mergeChannelStats(\n wooriTV, genieTV, ringoBiz, sns\n) + + note right of Service + **๋ฐ์ดํ„ฐ ํ†ตํ•ฉ** + - ์ด ๋…ธ์ถœ ์ˆ˜ = ์™ธ๋ถ€ ์ฑ„๋„ ๋…ธ์ถœ ํ•ฉ๊ณ„ + - ์ด ์ฐธ์—ฌ์ž ์ˆ˜ = Analytics DB + - ์ฑ„๋„๋ณ„ ์ „ํ™˜์œจ = ์ฐธ์—ฌ์ž ์ˆ˜ / ๋…ธ์ถœ ์ˆ˜ + - ๋ง๊ณ ๋น„์ฆˆ: ํ†ตํ™” ์™„๋ฃŒ ์ˆ˜ ํฌํ•จ + end note + + Service -> Calculator: calculateROI(\n eventStats, channelStats\n) + activate Calculator + + note right of Calculator + **ROI ๊ณ„์‚ฐ ๋กœ์ง** + ์ด ๋น„์šฉ = ๊ฒฝํ’ˆ ๋น„์šฉ + ํ”Œ๋žซํผ ๋น„์šฉ + ์˜ˆ์ƒ ์ˆ˜์ต = ๋งค์ถœ ์ฆ๊ฐ€์•ก + ์‹ ๊ทœ ๊ณ ๊ฐ LTV + ROI = (์ˆ˜์ต - ๋น„์šฉ) / ๋น„์šฉ ร— 100 + end note + + Calculator --> Service: ROIData\n- roi: 250%\n- totalCost: 100๋งŒ์›\n- totalRevenue: 350๋งŒ์›\n- breakEvenPoint: ๋‹ฌ์„ฑ + deactivate Calculator + + Service -> Service: buildDashboardData(\n eventStats, channelStats, roiData\n) + + note right of Service + **๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์ƒ์„ฑ** + - 4๊ฐœ ์š”์•ฝ ์นด๋“œ + - ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ์ฐจํŠธ ๋ฐ์ดํ„ฐ + - ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด + - ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ถ„์„ + - ๋น„๊ต ๋ถ„์„ (์—…์ข… ํ‰๊ท , ์ด์ „ ์ด๋ฒคํŠธ) + end note + + ||| + == 4. Redis ์บ์‹ฑ ๋ฐ ์‘๋‹ต == + + Service -> Cache: set(\n "analytics:dashboard:{eventId}",\n dashboardData,\n TTL=3600\n) + activate Cache + + Cache -> Redis: ์บ์‹œ ์ €์žฅ\nSET analytics:dashboard:{eventId}\nvalue={ํ†ตํ•ฉ ๋ฐ์ดํ„ฐ}\nEX 3600 (1์‹œ๊ฐ„) + activate Redis + + Redis --> Cache: OK (์ €์žฅ ์™„๋ฃŒ) + deactivate Redis + + Cache --> Service: OK (์บ์‹ฑ ์™„๋ฃŒ) + deactivate Cache + + note right of Service + **์บ์‹ฑ ์™„๋ฃŒ** + - TTL: 3600์ดˆ (1์‹œ๊ฐ„) + - ๋‹ค์Œ ์กฐํšŒ ์‹œ Cache HIT + - ์˜ˆ์ƒ ํฌ๊ธฐ: 5KB + - ๊ฐฑ์‹  ์ฃผ๊ธฐ: 1์‹œ๊ฐ„๋งˆ๋‹ค ์ƒˆ ๋ฐ์ดํ„ฐ ์กฐํšŒ + end note + + Service --> Controller: DashboardResponse\n(200 OK) + deactivate Service + + Controller --> : 200 OK\nDashboard Data (JSON) + deactivate Controller + + note over Controller, DB + **Cache MISS ์‹œ๋‚˜๋ฆฌ์˜ค ์„ฑ๋Šฅ** + - ์‘๋‹ต ์‹œ๊ฐ„: ์•ฝ 3์ดˆ + - Analytics DB ์กฐํšŒ: 0.1์ดˆ + - ์™ธ๋ถ€ API ๋ณ‘๋ ฌ ํ˜ธ์ถœ: 2์ดˆ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) + - ROI ๊ณ„์‚ฐ: 0.05์ดˆ + - Redis ์บ์‹ฑ: 0.01์ดˆ + - ์ง๋ ฌํ™”/HTTP: 0.84์ดˆ + end note + +end + +@enduml diff --git a/design/backend/sequence/inner/analytics-๋ฐฐํฌ์™„๋ฃŒ๊ตฌ๋….puml b/design/backend/sequence/inner/analytics-๋ฐฐํฌ์™„๋ฃŒ๊ตฌ๋….puml new file mode 100644 index 0000000..11eb5fb --- /dev/null +++ b/design/backend/sequence/inner/analytics-๋ฐฐํฌ์™„๋ฃŒ๊ตฌ๋….puml @@ -0,0 +1,168 @@ +@startuml analytics-๋ฐฐํฌ์™„๋ฃŒ๊ตฌ๋… +!theme mono + +title Analytics Service - DistributionCompleted ์ด๋ฒคํŠธ ๊ตฌ๋… ์ฒ˜๋ฆฌ ๋‚ด๋ถ€ ์‹œํ€€์Šค\n(Kafka Event ๊ตฌ๋…) + +participant "Kafka Consumer" as Consumer +participant "DistributionCompletedListener" as Listener +participant "AnalyticsService" as Service +participant "AnalyticsRepository" as Repository +participant "CacheService" as Cache +participant "Redis" as Redis +database "Analytics DB" as DB + +note over Consumer + **Kafka Consumer ์„ค์ •** + - Topic: DistributionCompleted + - Consumer Group: analytics-service + - Partition Key: eventId + - At-Least-Once Delivery ๋ณด์žฅ +end note + +Kafka -> Consumer: DistributionCompleted ์ด๋ฒคํŠธ ์ˆ˜์‹ \n{\n eventId: "uuid",\n distributedChannels: [\n {\n channel: "์šฐ๋ฆฌ๋™๋„คTV",\n status: "SUCCESS",\n expectedViews: 5000\n },\n {\n channel: "์ง€๋‹ˆTV",\n status: "SUCCESS",\n expectedViews: 10000\n },\n {\n channel: "Instagram",\n status: "SUCCESS",\n expectedViews: 2000\n }\n ],\n completedAt: "2025-10-22T12:00:00Z"\n} +activate Consumer + +Consumer -> Listener: onDistributionCompleted(event) +activate Listener + +note right of Listener + **๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ** + - Redis Set์— ์ด๋ฒคํŠธ ID ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ + - ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€ + - Key: distribution_completed:{eventId} +end note + +Listener -> Redis: SISMEMBER distribution_completed {eventId} +activate Redis + +alt ์ด๋ฒคํŠธ ๋ฏธ์ฒ˜๋ฆฌ (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ) + Redis --> Listener: false (๋ฏธ์ฒ˜๋ฆฌ) + deactivate Redis + + Listener -> Service: updateDistributionStats(event) + activate Service + + note right of Service + **๋ฐฐํฌ ์ฑ„๋„ ํ†ต๊ณ„ ์ €์žฅ** + - ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ์ƒํƒœ ๊ธฐ๋ก + - ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜ ์ง‘๊ณ„ + - ๋ฐฐํฌ ์™„๋ฃŒ ์‹œ๊ฐ ๊ธฐ๋ก + end note + + Service -> Service: parseChannelStats(event) + + note right of Service + **์ฑ„๋„ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ** + - distributedChannels ๋ฐฐ์—ด ์ˆœํšŒ + - ๊ฐ ์ฑ„๋„๋ณ„ ํ†ต๊ณ„ ์ถ”์ถœ + - ์ด ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜ ๊ณ„์‚ฐ + end note + + loop ๊ฐ ์ฑ„๋„๋ณ„๋กœ + Service -> Repository: saveChannelStats(\n eventId, channel, stats\n) + activate Repository + + Repository -> DB: ์ฑ„๋„๋ณ„ ํ†ต๊ณ„ ์ €์žฅ\n(์ด๋ฒคํŠธID, ์ฑ„๋„๋ช…, ์ƒํƒœ,\n์˜ˆ์ƒ๋…ธ์ถœ์ˆ˜, ๋ฐฐํฌ์ผ์‹œ ์ €์žฅ,\n์ค‘๋ณต ์‹œ ์—…๋ฐ์ดํŠธ) + activate DB + + DB --> Repository: 1 row inserted/updated + deactivate DB + + Repository --> Service: ChannelStatsEntity + deactivate Repository + end + + note right of Service + **๋ฐฐํฌ ํ†ต๊ณ„ ์ €์žฅ ์™„๋ฃŒ** + - ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ์ƒํƒœ ๊ธฐ๋ก + - ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜ ์ €์žฅ + - ํ–ฅํ›„ ์™ธ๋ถ€ API ์กฐํšŒ ์‹œ ๊ธฐ์ค€ ๋ฐ์ดํ„ฐ๋กœ ํ™œ์šฉ + end note + + Service -> Repository: updateTotalViews(eventId, totalViews) + activate Repository + + Repository -> DB: ์ด ๋…ธ์ถœ ์ˆ˜ ์—…๋ฐ์ดํŠธ\n(์ด ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜๋ฅผ ์„ค์ •ํ•˜๊ณ ,\n์ˆ˜์ •์ผ์‹œ๋ฅผ ํ˜„์žฌ ์‹œ๊ฐ์œผ๋กœ ์—…๋ฐ์ดํŠธ) + activate DB + + DB --> Repository: 1 row updated + deactivate DB + + Repository --> Service: UpdateResult (success) + deactivate Repository + + note right of Service + **์ด๋ฒคํŠธ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ** + - ์ด ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜ ์—…๋ฐ์ดํŠธ + - ๋‹ค์Œ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ ์‹œ ๋ฐ˜์˜ + end note + + Service -> Cache: delete("analytics:dashboard:{eventId}") + activate Cache + + note right of Cache + **์บ์‹œ ๋ฌดํšจํ™”** + - ๊ธฐ์กด ์บ์‹œ ์‚ญ์ œ + - ๋‹ค์Œ ์กฐํšŒ ์‹œ ์ตœ์‹  ๋ฐฐํฌ ํ†ต๊ณ„ ๋ฐ˜์˜ + - ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ์ฐจํŠธ ๊ฐฑ์‹  + end note + + Cache -> Redis: DEL analytics:dashboard:{eventId} + activate Redis + + Redis --> Cache: OK + deactivate Redis + + Cache --> Service: OK + deactivate Cache + + Service -> Redis: SADD distribution_completed {eventId} + activate Redis + + note right of Redis + **๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ๊ธฐ๋ก** + - Redis Set์— eventId ์ถ”๊ฐ€ + - TTL ์„ค์ • (7์ผ) + end note + + Redis --> Service: OK + deactivate Redis + + Service --> Listener: ๋ฐฐํฌ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ + deactivate Service + + Listener -> Consumer: ACK (์ฒ˜๋ฆฌ ์™„๋ฃŒ) + deactivate Listener + +else ์ด๋ฒคํŠธ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ (์ค‘๋ณต) + Redis --> Listener: true (์ด๋ฏธ ์ฒ˜๋ฆฌ) + deactivate Redis + + note right of Listener + **์ค‘๋ณต ์ด๋ฒคํŠธ ์Šคํ‚ต** + - At-Least-Once Delivery๋กœ ์ธํ•œ ์ค‘๋ณต + - ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์œผ๋กœ ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€ + end note + + Listener -> Consumer: ACK (์Šคํ‚ต) + deactivate Listener +end + +Consumer --> Kafka: Commit Offset +deactivate Consumer + +note over Consumer, DB + **์ฒ˜๋ฆฌ ์‹œ๊ฐ„** + - ์ด๋ฒคํŠธ ์ˆ˜์‹  โ†’ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ์•ฝ 0.3์ดˆ + - ์ฑ„๋„๋ณ„ DB INSERT (3๊ฐœ): 0.15์ดˆ + - event_stats UPDATE: 0.05์ดˆ + - Redis ์บ์‹œ ๋ฌดํšจํ™”: 0.01์ดˆ + - ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ: 0.01์ดˆ + + **๋ฐฐํฌ ํ†ต๊ณ„ ํšจ๊ณผ** + - ๋ฐฐํฌ ์™„๋ฃŒ ์ฆ‰์‹œ ํ†ต๊ณ„ ๋ฐ˜์˜ + - ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ์ถ”์  ๊ฐ€๋Šฅ + - ๋‹ค์Œ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ ์‹œ ์ตœ์‹  ๋ฐฐํฌ ์ •๋ณด ์ œ๊ณต +end note + +@enduml diff --git a/design/backend/sequence/inner/analytics-์ด๋ฒคํŠธ์ƒ์„ฑ๊ตฌ๋….puml b/design/backend/sequence/inner/analytics-์ด๋ฒคํŠธ์ƒ์„ฑ๊ตฌ๋….puml new file mode 100644 index 0000000..cad097b --- /dev/null +++ b/design/backend/sequence/inner/analytics-์ด๋ฒคํŠธ์ƒ์„ฑ๊ตฌ๋….puml @@ -0,0 +1,134 @@ +@startuml analytics-์ด๋ฒคํŠธ์ƒ์„ฑ๊ตฌ๋… +!theme mono + +title Analytics Service - EventCreated ์ด๋ฒคํŠธ ๊ตฌ๋… ์ฒ˜๋ฆฌ ๋‚ด๋ถ€ ์‹œํ€€์Šค\n(Kafka Event ๊ตฌ๋…) + +participant "Kafka Consumer" as Consumer +participant "EventCreatedListener" as Listener +participant "AnalyticsService" as Service +participant "AnalyticsRepository" as Repository +participant "CacheService" as Cache +participant "Redis" as Redis +database "Analytics DB" as DB + +note over Consumer + **Kafka Consumer ์„ค์ •** + - Topic: EventCreated + - Consumer Group: analytics-service + - Partition Key: eventId + - At-Least-Once Delivery ๋ณด์žฅ +end note + +Kafka -> Consumer: EventCreated ์ด๋ฒคํŠธ ์ˆ˜์‹ \n{\n eventId: "uuid",\n storeId: "uuid",\n title: "์ด๋ฒคํŠธ ์ œ๋ชฉ",\n objective: "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜",\n createdAt: "2025-10-22T10:00:00Z"\n} +activate Consumer + +Consumer -> Listener: onEventCreated(event) +activate Listener + +note right of Listener + **๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ** + - Redis Set์— ์ด๋ฒคํŠธ ID ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ + - ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€ +end note + +Listener -> Redis: SISMEMBER processed_events {eventId} +activate Redis + +alt ์ด๋ฒคํŠธ ๋ฏธ์ฒ˜๋ฆฌ (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ) + Redis --> Listener: false (๋ฏธ์ฒ˜๋ฆฌ) + deactivate Redis + + Listener -> Service: initializeEventStats(event) + activate Service + + note right of Service + **์ด๋ฒคํŠธ ํ†ต๊ณ„ ์ดˆ๊ธฐํ™”** + - ์ด๋ฒคํŠธ ๊ธฐ๋ณธ ์ •๋ณด ์ €์žฅ + - ํ†ต๊ณ„ ์ดˆ๊ธฐ๊ฐ’ ์„ค์ • + * ์ด ์ฐธ์—ฌ์ž ์ˆ˜: 0 + * ์ด ๋…ธ์ถœ ์ˆ˜: 0 + * ์˜ˆ์ƒ ROI: ๊ณ„์‚ฐ ์ „ + * ๋งค์ถœ ์ฆ๊ฐ€์œจ: 0% + end note + + Service -> Repository: save(eventStatsEntity) + activate Repository + + Repository -> DB: ์ด๋ฒคํŠธ ํ†ต๊ณ„ ์ดˆ๊ธฐํ™”\n(์ด๋ฒคํŠธID, ๋งค์žฅID, ์ œ๋ชฉ, ๋ชฉ์ ,\n์ฐธ์—ฌ์ž์ˆ˜/๋…ธ์ถœ์ˆ˜/ROI/๋งค์ถœ์ฆ๊ฐ€์œจ์„\n0์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•˜์—ฌ ์ €์žฅ) + activate DB + + DB --> Repository: 1 row inserted + deactivate DB + + Repository --> Service: EventStatsEntity + deactivate Repository + + note right of Service + **์ดˆ๊ธฐํ™” ์™„๋ฃŒ** + - ์ด๋ฒคํŠธ ํ†ต๊ณ„ DB ์ƒ์„ฑ + - ํ–ฅํ›„ ParticipantRegistered ์ด๋ฒคํŠธ ์ˆ˜์‹  ์‹œ + ์‹ค์‹œ๊ฐ„ ์ฆ๊ฐ€ + end note + + Service -> Cache: delete("analytics:dashboard:{eventId}") + activate Cache + + note right of Cache + **์บ์‹œ ๋ฌดํšจํ™”** + - ๊ธฐ์กด ์บ์‹œ ์‚ญ์ œ + - ๋‹ค์Œ ์กฐํšŒ ์‹œ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  + end note + + Cache -> Redis: DEL analytics:dashboard:{eventId} + activate Redis + + Redis --> Cache: OK + deactivate Redis + + Cache --> Service: OK + deactivate Cache + + Service -> Redis: SADD processed_events {eventId} + activate Redis + + note right of Redis + **๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ๊ธฐ๋ก** + - Redis Set์— eventId ์ถ”๊ฐ€ + - TTL ์„ค์ • (7์ผ) + end note + + Redis --> Service: OK + deactivate Redis + + Service --> Listener: EventStats ์ดˆ๊ธฐํ™” ์™„๋ฃŒ + deactivate Service + + Listener -> Consumer: ACK (์ฒ˜๋ฆฌ ์™„๋ฃŒ) + deactivate Listener + +else ์ด๋ฒคํŠธ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ (์ค‘๋ณต) + Redis --> Listener: true (์ด๋ฏธ ์ฒ˜๋ฆฌ) + deactivate Redis + + note right of Listener + **์ค‘๋ณต ์ด๋ฒคํŠธ ์Šคํ‚ต** + - At-Least-Once Delivery๋กœ ์ธํ•œ ์ค‘๋ณต + - ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์œผ๋กœ ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€ + end note + + Listener -> Consumer: ACK (์Šคํ‚ต) + deactivate Listener +end + +Consumer --> Kafka: Commit Offset +deactivate Consumer + +note over Consumer, DB + **์ฒ˜๋ฆฌ ์‹œ๊ฐ„** + - ์ด๋ฒคํŠธ ์ˆ˜์‹  โ†’ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ: ์•ฝ 0.2์ดˆ + - DB INSERT: 0.05์ดˆ + - Redis ์บ์‹œ ๋ฌดํšจํ™”: 0.01์ดˆ + - ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ: 0.01์ดˆ +end note + +@enduml diff --git a/design/backend/sequence/inner/analytics-์ฐธ์—ฌ์ž๋“ฑ๋ก๊ตฌ๋….puml b/design/backend/sequence/inner/analytics-์ฐธ์—ฌ์ž๋“ฑ๋ก๊ตฌ๋….puml new file mode 100644 index 0000000..aa2d680 --- /dev/null +++ b/design/backend/sequence/inner/analytics-์ฐธ์—ฌ์ž๋“ฑ๋ก๊ตฌ๋….puml @@ -0,0 +1,135 @@ +@startuml analytics-์ฐธ์—ฌ์ž๋“ฑ๋ก๊ตฌ๋… +!theme mono + +title Analytics Service - ParticipantRegistered ์ด๋ฒคํŠธ ๊ตฌ๋… ์ฒ˜๋ฆฌ ๋‚ด๋ถ€ ์‹œํ€€์Šค\n(Kafka Event ๊ตฌ๋…) + +participant "Kafka Consumer" as Consumer +participant "ParticipantRegisteredListener" as Listener +participant "AnalyticsService" as Service +participant "AnalyticsRepository" as Repository +participant "CacheService" as Cache +participant "Redis" as Redis +database "Analytics DB" as DB + +note over Consumer + **Kafka Consumer ์„ค์ •** + - Topic: ParticipantRegistered + - Consumer Group: analytics-service + - Partition Key: eventId + - At-Least-Once Delivery ๋ณด์žฅ +end note + +Kafka -> Consumer: ParticipantRegistered ์ด๋ฒคํŠธ ์ˆ˜์‹ \n{\n participantId: "uuid",\n eventId: "uuid",\n phoneNumber: "010-1234-5678",\n registeredAt: "2025-10-22T11:30:00Z"\n} +activate Consumer + +Consumer -> Listener: onParticipantRegistered(event) +activate Listener + +note right of Listener + **๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ** + - Redis Set์— participantId ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ + - ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€ +end note + +Listener -> Redis: SISMEMBER processed_participants {participantId} +activate Redis + +alt ์ด๋ฒคํŠธ ๋ฏธ์ฒ˜๋ฆฌ (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ) + Redis --> Listener: false (๋ฏธ์ฒ˜๋ฆฌ) + deactivate Redis + + Listener -> Service: updateParticipantCount(eventId) + activate Service + + note right of Service + **์ฐธ์—ฌ์ž ์ˆ˜ ์‹ค์‹œ๊ฐ„ ์ฆ๊ฐ€** + - DB UPDATE๋กœ ์ฐธ์—ฌ์ž ์ˆ˜ ์ฆ๊ฐ€ + - ์บ์‹œ ๋ฌดํšจํ™”๋กœ ๋‹ค์Œ ์กฐํšŒ ์‹œ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๋ฐ˜์˜ + end note + + Service -> Repository: incrementParticipantCount(eventId) + activate Repository + + Repository -> DB: ์ฐธ์—ฌ์ž ์ˆ˜ ์ฆ๊ฐ€\n(์ฐธ์—ฌ์ž ์ˆ˜๋ฅผ 1 ์ฆ๊ฐ€์‹œํ‚ค๊ณ ,\n์ˆ˜์ •์ผ์‹œ๋ฅผ ํ˜„์žฌ ์‹œ๊ฐ์œผ๋กœ ์—…๋ฐ์ดํŠธ) + activate DB + + DB --> Repository: 1 row updated + deactivate DB + + Repository --> Service: UpdateResult (success) + deactivate Repository + + note right of Service + **์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ** + - ์ฐธ์—ฌ์ž ์ˆ˜ +1 + - ๋‹ค์Œ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ ์‹œ ์ตœ์‹  ํ†ต๊ณ„ ๋ฐ˜์˜ + end note + + Service -> Cache: delete("analytics:dashboard:{eventId}") + activate Cache + + note right of Cache + **์บ์‹œ ๋ฌดํšจํ™”** + - ๊ธฐ์กด ์บ์‹œ ์‚ญ์ œ + - ๋‹ค์Œ ์กฐํšŒ ์‹œ ์ตœ์‹  ์ฐธ์—ฌ์ž ์ˆ˜ ๋ฐ˜์˜ + - Cache MISS ์‹œ DB ์กฐํšŒ๋กœ ์ตœ์‹  ๋ฐ์ดํ„ฐ ํ™•๋ณด + end note + + Cache -> Redis: DEL analytics:dashboard:{eventId} + activate Redis + + Redis --> Cache: OK + deactivate Redis + + Cache --> Service: OK + deactivate Cache + + Service -> Redis: SADD processed_participants {participantId} + activate Redis + + note right of Redis + **๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ๊ธฐ๋ก** + - Redis Set์— participantId ์ถ”๊ฐ€ + - TTL ์„ค์ • (7์ผ) + end note + + Redis --> Service: OK + deactivate Redis + + Service --> Listener: ์ฐธ์—ฌ์ž ์ˆ˜ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ + deactivate Service + + Listener -> Consumer: ACK (์ฒ˜๋ฆฌ ์™„๋ฃŒ) + deactivate Listener + +else ์ด๋ฒคํŠธ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ (์ค‘๋ณต) + Redis --> Listener: true (์ด๋ฏธ ์ฒ˜๋ฆฌ) + deactivate Redis + + note right of Listener + **์ค‘๋ณต ์ด๋ฒคํŠธ ์Šคํ‚ต** + - At-Least-Once Delivery๋กœ ์ธํ•œ ์ค‘๋ณต + - ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์œผ๋กœ ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€ + end note + + Listener -> Consumer: ACK (์Šคํ‚ต) + deactivate Listener +end + +Consumer --> Kafka: Commit Offset +deactivate Consumer + +note over Consumer, DB + **์ฒ˜๋ฆฌ ์‹œ๊ฐ„** + - ์ด๋ฒคํŠธ ์ˆ˜์‹  โ†’ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ์•ฝ 0.15์ดˆ + - DB UPDATE: 0.05์ดˆ + - Redis ์บ์‹œ ๋ฌดํšจํ™”: 0.01์ดˆ + - ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ: 0.01์ดˆ + + **์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ํšจ๊ณผ** + - ์ฐธ์—ฌ์ž ๋“ฑ๋ก ์ฆ‰์‹œ ํ†ต๊ณ„ ๋ฐ˜์˜ + - ๋‹ค์Œ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ ์‹œ ์ตœ์‹  ๋ฐ์ดํ„ฐ ์ œ๊ณต + - Cache-Aside ํŒจํ„ด์œผ๋กœ ์„ฑ๋Šฅ ์œ ์ง€ +end note + +@enduml diff --git a/design/backend/sequence/inner/content-์ด๋ฏธ์ง€๊ฒฐ๊ณผ์กฐํšŒ.puml b/design/backend/sequence/inner/content-์ด๋ฏธ์ง€๊ฒฐ๊ณผ์กฐํšŒ.puml new file mode 100644 index 0000000..c9265e9 --- /dev/null +++ b/design/backend/sequence/inner/content-์ด๋ฏธ์ง€๊ฒฐ๊ณผ์กฐํšŒ.puml @@ -0,0 +1,140 @@ +@startuml event-์ด๋ฏธ์ง€๊ฒฐ๊ณผ์กฐํšŒ +!theme mono + +title Content Service - ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๊ฒฐ๊ณผ ํด๋ง ์กฐํšŒ + +actor Client +participant "API Gateway" as Gateway +participant "ContentController" as Controller <> +participant "ContentService" as Service <> +participant "JobManager" as JobMgr <> +participant "Redis Cache" as Cache <> + +note over Controller, Cache +**ํด๋ง ๋ฐฉ์‹ Job ์ƒํƒœ ์กฐํšŒ** +- ์ตœ๋Œ€ 30์ดˆ ๋™์•ˆ ํด๋ง (2์ดˆ ๊ฐ„๊ฒฉ) +- Job ์ƒํƒœ: PENDING โ†’ PROCESSING โ†’ COMPLETED +- ์ด๋ฏธ์ง€ URL: Redis์— ์ €์žฅ (TTL: 7์ผ) +end note + +Client -> Gateway: GET /api/content/jobs/{jobId}/status +activate Gateway + +Gateway -> Controller: GET /api/content/jobs/{jobId}/status +activate Controller + +Controller -> Service: getJobStatus(jobId) +activate Service + +Service -> JobMgr: getJobStatus(jobId) +activate JobMgr + +JobMgr -> Cache: Job ์ƒํƒœ ์กฐํšŒ\nKey: job:{jobId} +activate Cache + +alt Job ๋ฐ์ดํ„ฐ ์กด์žฌ + Cache --> JobMgr: Job ๋ฐ์ดํ„ฐ\n{status, eventDraftId,\ntype, createdAt} + deactivate Cache + + alt status = COMPLETED + JobMgr -> Cache: ์ด๋ฏธ์ง€ URL ์กฐํšŒ\nKey: content:image:{eventDraftId} + activate Cache + Cache --> JobMgr: ์ด๋ฏธ์ง€ URL\n{simple, fancy, trendy} + deactivate Cache + + JobMgr --> Service: JobStatusResponse\n{jobId, status: COMPLETED,\nimageUrls: {...}} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: COMPLETED, imageUrls} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "COMPLETED",\n"imageUrls": {\n "simple": "https://cdn.../simple.png",\n "fancy": "https://cdn.../fancy.png",\n "trendy": "https://cdn.../trendy.png"\n}} + deactivate Controller + + Gateway --> Client: 200 OK\n์ด๋ฏธ์ง€ URL ๋ฐ˜ํ™˜ + deactivate Gateway + + note right of Client + **ํ”„๋ก ํŠธ์—”๋“œ ์ฒ˜๋ฆฌ** + - 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์นด๋“œ ํ‘œ์‹œ + - ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํƒ€์ผ ์„ ํƒ + - ์ด๋ฏธ์ง€ ํŽธ์ง‘ ๊ฐ€๋Šฅ + end note + + else status = PROCESSING ๋˜๋Š” PENDING + JobMgr --> Service: JobStatusResponse\n{jobId, status: PROCESSING} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: PROCESSING} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "PROCESSING",\n"message": "์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘์ž…๋‹ˆ๋‹ค"} + deactivate Controller + + Gateway --> Client: 200 OK\n์ง„ํ–‰ ์ค‘ ์ƒํƒœ + deactivate Gateway + + note right of Client + **ํด๋ง ์žฌ์‹œ๋„** + - 2์ดˆ ํ›„ ์žฌ์š”์ฒญ + - ์ตœ๋Œ€ 30์ดˆ (15ํšŒ) + end note + + else status = FAILED + JobMgr -> Cache: ์—๋Ÿฌ ์ •๋ณด ์กฐํšŒ\nKey: job:{jobId}:error + activate Cache + Cache --> JobMgr: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + deactivate Cache + + JobMgr --> Service: JobStatusResponse\n{jobId, status: FAILED, error} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: FAILED, error} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "FAILED",\n"error": "์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹คํŒจ",\n"message": "๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”"} + deactivate Controller + + Gateway --> Client: 200 OK\n์‹คํŒจ ์ƒํƒœ + deactivate Gateway + + note right of Client + **์‹คํŒจ ์ฒ˜๋ฆฌ** + - ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + - "๋‹ค์‹œ ์ƒ์„ฑ" ๋ฒ„ํŠผ ์ œ๊ณต + end note + end + +else Job ๋ฐ์ดํ„ฐ ์—†์Œ + Cache --> JobMgr: null (์บ์‹œ ๋ฏธ์Šค) + deactivate Cache + + JobMgr --> Service: throw NotFoundException\n("Job์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + deactivate JobMgr + + Service --> Controller: NotFoundException + deactivate Service + + Controller --> Gateway: 404 Not Found\n{"code": "JOB_001",\n"message": "Job์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"} + deactivate Controller + + Gateway --> Client: 404 Not Found + deactivate Gateway +end + +note over Controller, Cache +**ํด๋ง ์ „๋žต** +- ๊ฐ„๊ฒฉ: 2์ดˆ +- ์ตœ๋Œ€ ์‹œ๊ฐ„: 30์ดˆ (15ํšŒ) +- Timeout ์‹œ: ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ + "๋‹ค์‹œ ์ƒ์„ฑ" ์˜ต์…˜ + +**Redis ์บ์‹œ** +- Job ์ƒํƒœ: TTL 1์‹œ๊ฐ„ +- ์ด๋ฏธ์ง€ URL: TTL 7์ผ + +**์„ฑ๋Šฅ ๋ชฉํ‘œ** +- ํ‰๊ท  ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ๊ฐ„: 20์ดˆ ์ด๋‚ด +- P95 ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ๊ฐ„: 40์ดˆ ์ด๋‚ด +end note + +@enduml diff --git a/design/backend/sequence/inner/content-์ด๋ฏธ์ง€์ƒ์„ฑ.puml b/design/backend/sequence/inner/content-์ด๋ฏธ์ง€์ƒ์„ฑ.puml new file mode 100644 index 0000000..f00284d --- /dev/null +++ b/design/backend/sequence/inner/content-์ด๋ฏธ์ง€์ƒ์„ฑ.puml @@ -0,0 +1,255 @@ +@startuml content-์ด๋ฏธ์ง€์ƒ์„ฑ +!theme mono + +title Content Service - ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋‚ด๋ถ€ ์‹œํ€€์Šค (UFR-CONT-010) + +actor Client +participant "Kafka\nimage-job\nConsumer" as Consumer +participant "JobHandler" as Handler +participant "CacheManager" as Cache +participant "ImageGenerator" as Generator +participant "ImageStyleFactory" as Factory +participant "StableDiffusion\nAPI Client" as SDClient +participant "DALL-E\nAPI Client" as DALLEClient +participant "Circuit Breaker" as CB +participant "BlobStorage\nUploader" as BlobStorage +participant "JobStatusManager" as JobStatus +database "Redis Cache" as Redis + +note over Consumer: Kafka ๊ตฌ๋…\nimage-job ํ† ํ”ฝ + +== Kafka Job ์ˆ˜์‹  == +Consumer -> Handler: Job Message ์ˆ˜์‹ \n{jobId, eventDraftId, eventInfo} +activate Handler + +Handler -> Cache: ์บ์‹œ ์กฐํšŒ\nkey: content:image:{eventDraftId} +activate Cache +Cache -> Redis: GET content:image:{eventDraftId} +Redis --> Cache: ์บ์‹œ ๋ฐ์ดํ„ฐ ๋˜๋Š” NULL +Cache --> Handler: ์บ์‹œ ๊ฒฐ๊ณผ +deactivate Cache + +alt ์บ์‹œ HIT (๊ธฐ์กด ์ด๋ฏธ์ง€ ์กด์žฌ) + Handler -> JobStatus: Job ์ƒํƒœ ์—…๋ฐ์ดํŠธ\nstatus: COMPLETED (์บ์‹œ) + activate JobStatus + JobStatus -> Redis: SET job:{jobId}\n{status: COMPLETED, imageUrls: [...]} + JobStatus --> Handler: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ + deactivate JobStatus + Handler --> Consumer: ์ฒ˜๋ฆฌ ์™„๋ฃŒ (์บ์‹œ) +else ์บ์‹œ MISS (์ƒˆ๋กœ์šด ์ด๋ฏธ์ง€ ์ƒ์„ฑ) + Handler -> JobStatus: Job ์ƒํƒœ ์—…๋ฐ์ดํŠธ\nstatus: PROCESSING + activate JobStatus + JobStatus -> Redis: SET job:{jobId}\n{status: PROCESSING} + JobStatus --> Handler: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ + deactivate JobStatus + + Handler -> Generator: 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ\n{eventInfo} + activate Generator + + == 3๊ฐ€์ง€ ์Šคํƒ€์ผ ๋ณ‘๋ ฌ ์ƒ์„ฑ (par ๋ธ”๋ก) == + group parallel + Generator -> Factory: ์‹ฌํ”Œ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ\n{eventInfo, style: SIMPLE} + activate Factory + Factory --> Generator: ์‹ฌํ”Œ ํ”„๋กฌํ”„ํŠธ + deactivate Factory + + Generator -> CB: Circuit Breaker ์ฒดํฌ + activate CB + CB --> Generator: State: CLOSED (์ •์ƒ) + deactivate CB + + Generator -> SDClient: Stable Diffusion API ํ˜ธ์ถœ\n{prompt, style: SIMPLE}\nTimeout: 20์ดˆ + activate SDClient + + note over SDClient: Circuit Breaker ์ ์šฉ\nRetry: ์ตœ๋Œ€ 3ํšŒ\nTimeout: 20์ดˆ + + alt API ์„ฑ๊ณต + SDClient --> Generator: ์‹ฌํ”Œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ + deactivate SDClient + Generator -> BlobStorage: Blob ์—…๋กœ๋“œ ์š”์ฒญ\n{imageData, eventId, style: SIMPLE}\nRetry: 3ํšŒ, Timeout: 30์ดˆ + activate BlobStorage + note right of BlobStorage: SAS Token ์ƒ์„ฑ\n(์œ ํšจ๊ธฐ๊ฐ„ 7์ผ) + BlobStorage --> Generator: Blob SAS URL (์‹ฌํ”Œ) + deactivate BlobStorage + else API ์‹คํŒจ (Timeout/Error) + SDClient --> Generator: ์‹คํŒจ ์‘๋‹ต + deactivate SDClient + Generator -> CB: ์‹คํŒจ ๊ธฐ๋ก + activate CB + CB -> CB: ์‹คํŒจ์œจ ๊ณ„์‚ฐ + alt ์‹คํŒจ์œจ > 50% + CB -> CB: Circuit State: OPEN + end + CB --> Generator: Circuit State + deactivate CB + + Generator -> DALLEClient: Fallback - DALL-E API ํ˜ธ์ถœ\n{prompt, style: SIMPLE}\nTimeout: 20์ดˆ + activate DALLEClient + alt Fallback ์„ฑ๊ณต + DALLEClient --> Generator: ์‹ฌํ”Œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ + deactivate DALLEClient + Generator -> BlobStorage: Blob ์—…๋กœ๋“œ ์š”์ฒญ\n{imageData, eventId, style: SIMPLE}\nRetry: 3ํšŒ, Timeout: 30์ดˆ + activate BlobStorage + note right of BlobStorage: SAS Token ์ƒ์„ฑ\n(์œ ํšจ๊ธฐ๊ฐ„ 7์ผ) + BlobStorage --> Generator: Blob SAS URL (์‹ฌํ”Œ) + deactivate BlobStorage + else Fallback ์‹คํŒจ + DALLEClient --> Generator: ์‹คํŒจ ์‘๋‹ต + deactivate DALLEClient + Generator -> Generator: ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ\n(์‹ฌํ”Œ) + end + end + + Generator -> Factory: ํ™”๋ คํ•œ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ\n{eventInfo, style: FANCY} + activate Factory + Factory --> Generator: ํ™”๋ คํ•œ ํ”„๋กฌํ”„ํŠธ + deactivate Factory + + Generator -> CB: Circuit Breaker ์ฒดํฌ + activate CB + CB --> Generator: State: CLOSED/OPEN + deactivate CB + + alt Circuit CLOSED + Generator -> SDClient: Stable Diffusion API ํ˜ธ์ถœ\n{prompt, style: FANCY}\nTimeout: 20์ดˆ + activate SDClient + + alt API ์„ฑ๊ณต + SDClient --> Generator: ํ™”๋ คํ•œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ + deactivate SDClient + Generator -> BlobStorage: Blob ์—…๋กœ๋“œ ์š”์ฒญ\n{imageData, eventId, style: FANCY}\nRetry: 3ํšŒ, Timeout: 30์ดˆ + activate BlobStorage + note right of BlobStorage: SAS Token ์ƒ์„ฑ\n(์œ ํšจ๊ธฐ๊ฐ„ 7์ผ) + BlobStorage --> Generator: Blob SAS URL (ํ™”๋ คํ•œ) + deactivate BlobStorage + else API ์‹คํŒจ + SDClient --> Generator: ์‹คํŒจ ์‘๋‹ต + deactivate SDClient + Generator -> DALLEClient: Fallback - DALL-E API ํ˜ธ์ถœ + activate DALLEClient + alt Fallback ์„ฑ๊ณต + DALLEClient --> Generator: ํ™”๋ คํ•œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ + deactivate DALLEClient + Generator -> BlobStorage: Blob ์—…๋กœ๋“œ\n{imageData, eventId, style: FANCY}\nRetry: 3ํšŒ, Timeout: 30์ดˆ + activate BlobStorage + note right of BlobStorage: SAS Token ์ƒ์„ฑ\n(์œ ํšจ๊ธฐ๊ฐ„ 7์ผ) + BlobStorage --> Generator: Blob SAS URL (ํ™”๋ คํ•œ) + deactivate BlobStorage + else Fallback ์‹คํŒจ + DALLEClient --> Generator: ์‹คํŒจ ์‘๋‹ต + deactivate DALLEClient + Generator -> Generator: ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ\n(ํ™”๋ คํ•œ) + end + end + else Circuit OPEN + Generator -> Generator: Circuit Open\n์ฆ‰์‹œ ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ + end + + Generator -> Factory: ํŠธ๋ Œ๋”” ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ\n{eventInfo, style: TRENDY} + activate Factory + Factory --> Generator: ํŠธ๋ Œ๋”” ํ”„๋กฌํ”„ํŠธ + deactivate Factory + + Generator -> CB: Circuit Breaker ์ฒดํฌ + activate CB + CB --> Generator: State: CLOSED/OPEN + deactivate CB + + alt Circuit CLOSED + Generator -> SDClient: Stable Diffusion API ํ˜ธ์ถœ\n{prompt, style: TRENDY}\nTimeout: 20์ดˆ + activate SDClient + + alt API ์„ฑ๊ณต + SDClient --> Generator: ํŠธ๋ Œ๋”” ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ + deactivate SDClient + Generator -> BlobStorage: Blob ์—…๋กœ๋“œ ์š”์ฒญ\n{imageData, eventId, style: TRENDY}\nRetry: 3ํšŒ, Timeout: 30์ดˆ + activate BlobStorage + note right of BlobStorage: SAS Token ์ƒ์„ฑ\n(์œ ํšจ๊ธฐ๊ฐ„ 7์ผ) + BlobStorage --> Generator: Blob SAS URL (ํŠธ๋ Œ๋””) + deactivate BlobStorage + else API ์‹คํŒจ + SDClient --> Generator: ์‹คํŒจ ์‘๋‹ต + deactivate SDClient + Generator -> DALLEClient: Fallback - DALL-E API ํ˜ธ์ถœ + activate DALLEClient + alt Fallback ์„ฑ๊ณต + DALLEClient --> Generator: ํŠธ๋ Œ๋”” ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ + deactivate DALLEClient + Generator -> BlobStorage: Blob ์—…๋กœ๋“œ\n{imageData, eventId, style: TRENDY}\nRetry: 3ํšŒ, Timeout: 30์ดˆ + activate BlobStorage + note right of BlobStorage: SAS Token ์ƒ์„ฑ\n(์œ ํšจ๊ธฐ๊ฐ„ 7์ผ) + BlobStorage --> Generator: Blob SAS URL (ํŠธ๋ Œ๋””) + deactivate BlobStorage + else Fallback ์‹คํŒจ + DALLEClient --> Generator: ์‹คํŒจ ์‘๋‹ต + deactivate DALLEClient + Generator -> Generator: ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ\n(ํŠธ๋ Œ๋””) + end + end + else Circuit OPEN + Generator -> Generator: Circuit Open\n์ฆ‰์‹œ ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์‚ฌ์šฉ + end + end + + Generator --> Handler: 3๊ฐ€์ง€ ์ด๋ฏธ์ง€ URL ๋ฐ˜ํ™˜\n{simple, fancy, trendy} + deactivate Generator + + == ๊ฒฐ๊ณผ ์บ์‹ฑ ๋ฐ Job ์™„๋ฃŒ == + Handler -> Cache: Blob SAS URL ์บ์‹ฑ\nkey: content:image:{eventDraftId}\nTTL: 7์ผ + activate Cache + Cache -> Redis: SET content:image:{eventDraftId}\n{simple: SAS_URL, fancy: SAS_URL, trendy: SAS_URL}\nTTL: 604800 (7์ผ) + Redis --> Cache: ์ €์žฅ ์™„๋ฃŒ + Cache --> Handler: ์บ์‹ฑ ์™„๋ฃŒ + deactivate Cache + + Handler -> JobStatus: Job ์ƒํƒœ ์—…๋ฐ์ดํŠธ\nstatus: COMPLETED + activate JobStatus + JobStatus -> Redis: SET job:{jobId}\n{status: COMPLETED, imageUrls: [...]} + JobStatus --> Handler: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ + deactivate JobStatus + + Handler --> Consumer: ์ฒ˜๋ฆฌ ์™„๋ฃŒ + note over Handler + Blob SAS URL์€ Redis์—๋งŒ ์ €์žฅ๋จ + Event Service๋Š” ํด๋ง์„ ํ†ตํ•ด + Redis์—์„œ ๊ฒฐ๊ณผ ์กฐํšŒ + SAS Token ์œ ํšจ๊ธฐ๊ฐ„: 7์ผ + end note +end + +deactivate Handler + +note over Consumer, Redis +**Resilience ํŒจํ„ด ์ ์šฉ** +- Circuit Breaker: ์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open (AI API์šฉ) +- AI API Timeout: 20์ดˆ +- Fallback: Stable Diffusion ์‹คํŒจ ์‹œ DALL-E, ๋ชจ๋‘ ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ +- Blob Storage Retry: ์ตœ๋Œ€ 3ํšŒ (Exponential Backoff: 1s, 2s, 4s) +- Blob Storage Timeout: 30์ดˆ (๋Œ€์šฉ๋Ÿ‰ ์ด๋ฏธ์ง€ ๊ณ ๋ ค) +- Cache-Aside: Redis ์บ์‹ฑ (TTL 7์ผ) + +**์ฒ˜๋ฆฌ ์‹œ๊ฐ„** +- ์บ์‹œ HIT: 0.1์ดˆ +- ์บ์‹œ MISS: 5.2์ดˆ ์ด๋‚ด (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) + โ””โ”€ AI ์ƒ์„ฑ: 3-5์ดˆ + Blob ์—…๋กœ๋“œ: 0.15-0.3์ดˆ + +**๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ** +- 3๊ฐ€์ง€ ์Šคํƒ€์ผ ๋™์‹œ ์ƒ์„ฑ (par ๋ธ”๋ก) +- ๋…๋ฆฝ์ ์ธ ์Šค๋ ˆ๋“œ ํ’€ ์‚ฌ์šฉ + +**Blob Storage ์—…๋กœ๋“œ** +- Azure Blob Storage (Korea Central) +- SAS Token ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด (์ฝ๊ธฐ ์ „์šฉ) +- SAS Token ์œ ํšจ๊ธฐ๊ฐ„: 7์ผ (Redis TTL๊ณผ ๋™๊ธฐํ™”) +- Public Access ๋น„ํ™œ์„ฑํ™” (๋ณด์•ˆ ๊ฐ•ํ™”) +- Container: event-images +- URL ํ˜•์‹: https://{account}.blob.core.windows.net/event-images/{id}-{style}.png?{sas_token} + +**๋ณด์•ˆ** +- Storage Account Public Access ๋น„ํ™œ์„ฑํ™” +- SAS Token ๊ธฐ๋ฐ˜ URL ์ƒ์„ฑ (์ฝ๊ธฐ ์ „์šฉ ๊ถŒํ•œ) +- Firewall ๊ทœ์น™: K8s Cluster IP๋งŒ ํ—ˆ์šฉ +- HTTPS ๊ฐ•์ œ (TLS 1.2 ์ด์ƒ) +end note + +@enduml diff --git a/design/backend/sequence/inner/content-์ด๋ฏธ์ง€์ƒ์„ฑ์š”์ฒญ.puml b/design/backend/sequence/inner/content-์ด๋ฏธ์ง€์ƒ์„ฑ์š”์ฒญ.puml new file mode 100644 index 0000000..1d38c6a --- /dev/null +++ b/design/backend/sequence/inner/content-์ด๋ฏธ์ง€์ƒ์„ฑ์š”์ฒญ.puml @@ -0,0 +1,90 @@ +@startuml event-์ด๋ฏธ์ง€์ƒ์„ฑ์š”์ฒญ +!theme mono + +title Content Service - ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ (UFR-CONT-010) + +actor Client +participant "API Gateway" as Gateway +participant "ContentController" as Controller <> +participant "ContentService" as Service <> +participant "JobManager" as JobMgr <> +participant "Redis Cache" as Cache <> + +note over Controller, Cache +**UFR-CONT-010: SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ** +- Kafka ์‚ฌ์šฉ ์•ˆ ํ•จ (๋‚ด๋ถ€ Job ๊ด€๋ฆฌ) +- ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์›Œ์ปค๊ฐ€ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ +- Redis์—์„œ AI ์ถ”์ฒœ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ +- 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ (์‹ฌํ”Œ, ํ™”๋ คํ•œ, ํŠธ๋ Œ๋””) +end note + +Client -> Gateway: POST /api/content/images/{eventDraftId}/generate +activate Gateway + +Gateway -> Controller: POST /api/content/images/{eventDraftId}/generate +activate Controller + +Controller -> Controller: ์š”์ฒญ ๊ฒ€์ฆ\n(eventDraftId ์œ ํšจ์„ฑ) + +Controller -> Service: generateImages(eventDraftId) +activate Service + +== 1๋‹จ๊ณ„: Redis์—์„œ AI ์ถ”์ฒœ ๋ฐ์ดํ„ฐ ํ™•์ธ == + +Service -> Cache: AI ์ถ”์ฒœ ๋ฐ์ดํ„ฐ ์กฐํšŒ\nKey: ai:event:{eventDraftId} +activate Cache +Cache --> Service: AI ์ถ”์ฒœ ๊ฒฐ๊ณผ\n{์„ ํƒ๋œ ์ถ”์ฒœ์•ˆ, ์ด๋ฒคํŠธ ์ •๋ณด} +deactivate Cache + +alt AI ์ถ”์ฒœ ๋ฐ์ดํ„ฐ ์—†์Œ + Service --> Controller: throw NotFoundException\n("AI ์ถ”์ฒœ์„ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š”") + Controller --> Gateway: 404 Not Found\n{"code": "CONTENT_001",\n"message": "AI ์ถ”์ฒœ์„ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š”"} + deactivate Service + deactivate Controller + Gateway --> Client: 404 Not Found + deactivate Gateway + +else AI ์ถ”์ฒœ ๋ฐ์ดํ„ฐ ์กด์žฌ + + == 2๋‹จ๊ณ„: Job ์ƒ์„ฑ == + + Service -> JobMgr: createJob(eventDraftId, imageGeneration) + activate JobMgr + + JobMgr -> JobMgr: Job ID ์ƒ์„ฑ (UUID) + + JobMgr -> Cache: Job ์ƒํƒœ ์ €์žฅ\nKey: job:{jobId}\nValue: {status: PENDING,\neventDraftId, type: IMAGE_GEN,\ncreatedAt}\nTTL: 1์‹œ๊ฐ„ + activate Cache + Cache --> JobMgr: ์ €์žฅ ์™„๋ฃŒ + deactivate Cache + + JobMgr --> Service: Job ์ƒ์„ฑ ์™„๋ฃŒ\n{jobId, status: PENDING} + deactivate JobMgr + + == 3๋‹จ๊ณ„: ์‘๋‹ต ๋ฐ˜ํ™˜ == + + Service --> Controller: JobResponse\n{jobId, status: PENDING} + deactivate Service + + Controller --> Gateway: 202 Accepted\n{"jobId": "job-uuid-123",\n"status": "PENDING",\n"message": "์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘์ž…๋‹ˆ๋‹ค"} + deactivate Controller + + Gateway --> Client: 202 Accepted\n์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ์ž‘ + deactivate Gateway + + note over Service, Cache + **๋ฐฑ๊ทธ๋ผ์šด๋“œ ์›Œ์ปค ์ฒ˜๋ฆฌ** + - Redis ํด๋ง ๋˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ Job ๊ฐ์ง€ + - content-์ด๋ฏธ์ง€์ƒ์„ฑ.puml ์ฐธ์กฐ + - ์™ธ๋ถ€ ์ด๋ฏธ์ง€ ์ƒ์„ฑ API ํ˜ธ์ถœ (๋ณ‘๋ ฌ) + - Redis์— ์ด๋ฏธ์ง€ URL ์ €์žฅ + + **์ƒ์„ธ ๋‚ด์šฉ** + - 3๊ฐ€์ง€ ์Šคํƒ€์ผ ๋ณ‘๋ ฌ ์ƒ์„ฑ (์‹ฌํ”Œ, ํ™”๋ คํ•œ, ํŠธ๋ Œ๋””) + - Circuit Breaker ์ ์šฉ (Timeout: 5๋ถ„) + - ๊ฒฐ๊ณผ: Redis Key: content:image:{eventDraftId} + - TTL: 7์ผ + end note +end + +@enduml diff --git a/design/backend/sequence/inner/distribution-๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ.puml b/design/backend/sequence/inner/distribution-๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ.puml new file mode 100644 index 0000000..6f5eea9 --- /dev/null +++ b/design/backend/sequence/inner/distribution-๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ.puml @@ -0,0 +1,141 @@ +@startuml distribution-๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ-sprint2 +!theme mono + +title Distribution Service - ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ Sprint 2 (UFR-DIST-010) + +participant "Event Service" as EventSvc +participant "Distribution\nREST API" as API +participant "Distribution\nController" as Controller +participant "Distribution\nService" as Service +database "Distribution DB" as DB +queue "Kafka" as Kafka + +== REST API ๋™๊ธฐ ํ˜ธ์ถœ ์ˆ˜์‹  == +EventSvc -> API: POST /api/distribution/distribute\n{eventId, channels[], contentUrls} +activate API + +API -> Controller: distributeToChannels(request) +activate Controller + +Controller -> Service: executeDistribution(distributionRequest) +activate Service + +Service -> DB: ๋ฐฐํฌ ์ด๋ ฅ ์ดˆ๊ธฐํ™”\n(์ด๋ฒคํŠธID, ์ƒํƒœ๋ฅผ PENDING์œผ๋กœ ์ €์žฅ) +DB --> Service: ๋ฐฐํฌ ์ด๋ ฅ ID + +note over Service: ๋ฐฐํฌ ์‹œ์ž‘ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ +Service -> DB: ๋ฐฐํฌ ์ด๋ ฅ ์ƒํƒœ ์—…๋ฐ์ดํŠธ\n(์ƒํƒœ๋ฅผ IN_PROGRESS๋กœ ๋ณ€๊ฒฝ) + +== ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ ๋กœ๊ทธ ๊ธฐ๋ก (Sprint 2: Mock ์ฒ˜๋ฆฌ) == + +note over Service: Sprint 2: ์‹ค์ œ ์™ธ๋ถ€ API ํ˜ธ์ถœ ์—†์ด\n๋ฐฐํฌ ๊ฒฐ๊ณผ๋งŒ ๊ธฐ๋ก + +par ์šฐ๋ฆฌ๋™๋„คTV ๋ฐฐํฌ + alt ์ฑ„๋„ ์„ ํƒ๋จ + Service -> Service: ์šฐ๋ฆฌ๋™๋„คTV ๋ฐฐํฌ ์ฒ˜๋ฆฌ\n(Mock: ์ฆ‰์‹œ ์„ฑ๊ณต ๋ฐ˜ํ™˜) + activate Service + + note over Service: ๋ฐฐํฌ ์š”์ฒญ ๊ฒ€์ฆ\n- eventId ์œ ํšจ์„ฑ\n- contentUrls ์กด์žฌ ์—ฌ๋ถ€ + + Service -> DB: ๋ฐฐํฌ ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(์ฑ„๋„: ์šฐ๋ฆฌ๋™๋„คTV,\n์ƒํƒœ: ์„ฑ๊ณต, ๋ฐฐํฌID,\n์˜ˆ์ƒ๋…ธ์ถœ์ˆ˜ ์ €์žฅ) + + note over Service: Mock ๊ฒฐ๊ณผ:\n์„ฑ๊ณต (distributionId ์ƒ์„ฑ) + + deactivate Service + end + + alt ๋ง๊ณ ๋น„์ฆˆ ์„ ํƒ๋จ + Service -> Service: ๋ง๊ณ ๋น„์ฆˆ ๋ฐฐํฌ ์ฒ˜๋ฆฌ\n(Mock: ์ฆ‰์‹œ ์„ฑ๊ณต ๋ฐ˜ํ™˜) + activate Service + + note over Service: ๋ฐฐํฌ ์š”์ฒญ ๊ฒ€์ฆ\n- phoneNumber ํ˜•์‹\n- audioUrl ์กด์žฌ ์—ฌ๋ถ€ + + Service -> DB: ๋ฐฐํฌ ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(์ฑ„๋„: ๋ง๊ณ ๋น„์ฆˆ,\n์ƒํƒœ: ์„ฑ๊ณต,\n์—…๋ฐ์ดํŠธ ์‹œ๊ฐ ์ €์žฅ) + + note over Service: Mock ๊ฒฐ๊ณผ:\n์„ฑ๊ณต (timestamp ๊ธฐ๋ก) + + deactivate Service + end + + alt ์ง€๋‹ˆTV ์„ ํƒ๋จ + Service -> Service: ์ง€๋‹ˆTV ๋ฐฐํฌ ์ฒ˜๋ฆฌ\n(Mock: ์ฆ‰์‹œ ์„ฑ๊ณต ๋ฐ˜ํ™˜) + activate Service + + note over Service: ๋ฐฐํฌ ์š”์ฒญ ๊ฒ€์ฆ\n- region ์œ ํšจ์„ฑ\n- schedule ํ˜•์‹\n- budget ๋ฒ”์œ„ + + Service -> DB: ๋ฐฐํฌ ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(์ฑ„๋„: ์ง€๋‹ˆTV,\n์ƒํƒœ: ์„ฑ๊ณต, ๊ด‘๊ณ ID,\n๋…ธ์ถœ ์Šค์ผ€์ค„ ์ €์žฅ) + + note over Service: Mock ๊ฒฐ๊ณผ:\n์„ฑ๊ณต (adId ์ƒ์„ฑ) + + deactivate Service + end + + alt Instagram ์„ ํƒ๋จ + Service -> Service: Instagram ๋ฐฐํฌ ์ฒ˜๋ฆฌ\n(Mock: ์ฆ‰์‹œ ์„ฑ๊ณต ๋ฐ˜ํ™˜) + activate Service + + note over Service: ๋ฐฐํฌ ์š”์ฒญ ๊ฒ€์ฆ\n- imageUrl ํ˜•์‹\n- caption ๊ธธ์ด\n- hashtags ์œ ํšจ์„ฑ + + Service -> DB: ๋ฐฐํฌ ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(์ฑ„๋„: Instagram,\n์ƒํƒœ: ์„ฑ๊ณต,\nํฌ์ŠคํŠธ URL/ID ์ €์žฅ) + + note over Service: Mock ๊ฒฐ๊ณผ:\n์„ฑ๊ณต (postUrl, postId ์ƒ์„ฑ) + + deactivate Service + end + + alt Naver Blog ์„ ํƒ๋จ + Service -> Service: Naver Blog ๋ฐฐํฌ ์ฒ˜๋ฆฌ\n(Mock: ์ฆ‰์‹œ ์„ฑ๊ณต ๋ฐ˜ํ™˜) + activate Service + + note over Service: ๋ฐฐํฌ ์š”์ฒญ ๊ฒ€์ฆ\n- imageUrl ํ˜•์‹\n- content ๊ธธ์ด + + Service -> DB: ๋ฐฐํฌ ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(์ฑ„๋„: NaverBlog,\n์ƒํƒœ: ์„ฑ๊ณต,\nํฌ์ŠคํŠธ URL ์ €์žฅ) + + note over Service: Mock ๊ฒฐ๊ณผ:\n์„ฑ๊ณต (postUrl ์ƒ์„ฑ) + + deactivate Service + end + + alt Kakao Channel ์„ ํƒ๋จ + Service -> Service: Kakao Channel ๋ฐฐํฌ ์ฒ˜๋ฆฌ\n(Mock: ์ฆ‰์‹œ ์„ฑ๊ณต ๋ฐ˜ํ™˜) + activate Service + + note over Service: ๋ฐฐํฌ ์š”์ฒญ ๊ฒ€์ฆ\n- imageUrl ํ˜•์‹\n- message ๊ธธ์ด + + Service -> DB: ๋ฐฐํฌ ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(์ฑ„๋„: KakaoChannel,\n์ƒํƒœ: ์„ฑ๊ณต,\n๋ฉ”์‹œ์ง€ ID ์ €์žฅ) + + note over Service: Mock ๊ฒฐ๊ณผ:\n์„ฑ๊ณต (messageId ์ƒ์„ฑ) + + deactivate Service + end +end + +note over Service: ๋ชจ๋“  ์ฑ„๋„ ๋ฐฐํฌ ์™„๋ฃŒ\n(์ฆ‰์‹œ ์ฒ˜๋ฆฌ - ์™ธ๋ถ€ API ํ˜ธ์ถœ ์—†์Œ) + +== ๋ฐฐํฌ ๊ฒฐ๊ณผ ์ง‘๊ณ„ ๋ฐ ์ €์žฅ == +Service -> Service: ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ๊ฒฐ๊ณผ ์ง‘๊ณ„\n์„ฑ๊ณต: [์„ ํƒ๋œ ๋ชจ๋“  ์ฑ„๋„] + +note over Service: Sprint 2์—์„œ๋Š”\n๋ชจ๋“  ์ฑ„๋„ ๋ฐฐํฌ๊ฐ€ ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ๋จ + +Service -> DB: ๋ฐฐํฌ ์ด๋ ฅ ์ƒํƒœ ์—…๋ฐ์ดํŠธ\n(์ƒํƒœ๋ฅผ COMPLETED๋กœ,\n์™„๋ฃŒ์ผ์‹œ๋ฅผ ํ˜„์žฌ ์‹œ๊ฐ์œผ๋กœ ์„ค์ •) + +== Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ == +Service -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, channels[], results[], completedAt} +note over Kafka: Analytics Service ๊ตฌ๋…\n์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ + +== REST API ๋™๊ธฐ ์‘๋‹ต == +Service --> Controller: ๋ฐฐํฌ ์™„๋ฃŒ ์‘๋‹ต\n{status: COMPLETED, successChannels: [all]} +deactivate Service + +Controller --> API: DistributionResponse\n{eventId, status: COMPLETED, results: [all success]} +deactivate Controller + +API --> EventSvc: 200 OK\n{distributionId, status: COMPLETED, results[]} +deactivate API + +note over EventSvc: ๋ฐฐํฌ ์™„๋ฃŒ ์‘๋‹ต ์ˆ˜์‹ \n์ด๋ฒคํŠธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ\nAPPROVED โ†’ ACTIVE + +== Sprint 2 ์ œ์•ฝ์‚ฌํ•ญ == +note over Service: **Sprint 2 ๊ตฌํ˜„ ๋ฒ”์œ„**\n- ์™ธ๋ถ€ API ํ˜ธ์ถœ ์—†์Œ (Mock ์ฒ˜๋ฆฌ)\n- ๋ชจ๋“  ๋ฐฐํฌ ์š”์ฒญ์€ ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ\n- ๋ฐฐํฌ ๋กœ๊ทธ๋งŒ DB์— ๊ธฐ๋ก\n- Circuit Breaker, Retry ๋ฏธ๊ตฌํ˜„\n- ์‹คํŒจ ์ฒ˜๋ฆฌ ์‹œ๋‚˜๋ฆฌ์˜ค ๋ฏธ๊ตฌํ˜„\n\n**Sprint 3 ์ดํ›„ ๊ตฌํ˜„ ์˜ˆ์ •**\n- ์‹ค์ œ ์™ธ๋ถ€ ์ฑ„๋„ API ์—ฐ๋™\n- Circuit Breaker ํŒจํ„ด ์ ์šฉ\n- Retry ๋กœ์ง ๊ตฌํ˜„\n- ์‹คํŒจ ์ฒ˜๋ฆฌ ๋ฐ ์•Œ๋ฆผ + +@enduml diff --git a/design/backend/sequence/inner/event-AI์ถ”์ฒœ์š”์ฒญ.puml b/design/backend/sequence/inner/event-AI์ถ”์ฒœ์š”์ฒญ.puml new file mode 100644 index 0000000..f3baef0 --- /dev/null +++ b/design/backend/sequence/inner/event-AI์ถ”์ฒœ์š”์ฒญ.puml @@ -0,0 +1,126 @@ +@startuml event-AI์ถ”์ฒœ์š”์ฒญ +!theme mono + +title Event Service - AI ์ถ”์ฒœ ์š”์ฒญ (Kafka Job ๋ฐœํ–‰) (UFR-EVENT-030) + +actor Client +participant "API Gateway" as Gateway +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "JobService" as JobSvc <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> +participant "Kafka Producer" as Kafka <> + +note over Controller, Kafka +**UFR-EVENT-030: AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ์š”์ฒญ** +- Kafka ๋น„๋™๊ธฐ Job ๋ฐœํ–‰ +- AI Service๊ฐ€ Kafka ๊ตฌ๋…ํ•˜์—ฌ ์ฒ˜๋ฆฌ +- ํŠธ๋ Œ๋“œ ๋ถ„์„ + 3๊ฐ€์ง€ ์ถ”์ฒœ์•ˆ ์ƒ์„ฑ +- ์ฒ˜๋ฆฌ ์‹œ๊ฐ„: ํ‰๊ท  2๋ถ„ ์ด๋‚ด +end note + +Client -> Gateway: POST /api/events/{eventDraftId}/ai-recommendations\n{"objective": "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜",\n"industry": "์Œ์‹์ ",\n"region": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ"} +activate Gateway + +Gateway -> Controller: POST /api/events/{eventDraftId}/ai-recommendations +activate Controller + +Controller -> Controller: ์š”์ฒญ ๊ฒ€์ฆ\n(ํ•„์ˆ˜ ํ•„๋“œ, ๋ชฉ์  ์œ ํšจ์„ฑ) + +Controller -> Service: requestAIRecommendation(eventDraftId, userId) +activate Service + +== 1๋‹จ๊ณ„: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์กฐํšŒ ๋ฐ ๊ฒ€์ฆ == + +Service -> Repo: findById(eventDraftId) +activate Repo +Repo -> DB: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์กฐํšŒ\n(์ดˆ์•ˆID๋กœ ์ด๋ฒคํŠธ ๋ชฉ์ ,\n๋งค์žฅ ์ •๋ณด ์กฐํšŒ) +activate DB +DB --> Repo: EventDraft ์—”ํ‹ฐํ‹ฐ\n{๋ชฉ์ , ๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ} +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +Service -> Service: ์†Œ์œ ๊ถŒ ๊ฒ€์ฆ\nvalidateOwnership(userId, eventDraft) + +alt ์†Œ์œ ๊ถŒ ์—†์Œ + Service --> Controller: throw ForbiddenException\n("๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค") + Controller --> Gateway: 403 Forbidden\n{"code": "EVENT_003",\n"message": "๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค"} + deactivate Service + deactivate Controller + Gateway --> Client: 403 Forbidden + deactivate Gateway + +else ์†Œ์œ ๊ถŒ ํ™•์ธ + + == 2๋‹จ๊ณ„: Kafka Job ์ƒ์„ฑ == + + Service -> JobSvc: createAIJob(eventDraft) + activate JobSvc + + JobSvc -> JobSvc: Job ID ์ƒ์„ฑ (UUID) + + JobSvc -> Cache: Job ์ƒํƒœ ์ €์žฅ\nKey: job:{jobId}\nValue: {status: PENDING,\neventDraftId, type: AI_RECOMMEND,\ncreatedAt}\nTTL: 1์‹œ๊ฐ„ + activate Cache + Cache --> JobSvc: ์ €์žฅ ์™„๋ฃŒ + deactivate Cache + + == 3๋‹จ๊ณ„: Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ == + + JobSvc -> Kafka: ์ด๋ฒคํŠธ ๋ฐœํ–‰\nTopic: ai-job-topic\nPayload: {jobId, eventDraftId,\nobjective, industry,\nregion, storeInfo} + activate Kafka + note right of Kafka + **Kafka Topic** + - Topic: ai-job-topic + - Consumer: AI Service + - Consumer Group: ai-service-group + + **Payload** + { + "jobId": "UUID", + "eventDraftId": "UUID", + "objective": "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜", + "industry": "์Œ์‹์ ", + "region": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ", + "storeInfo": {...} + } + end note + Kafka --> JobSvc: ACK (๋ฐœํ–‰ ํ™•์ธ) + deactivate Kafka + + JobSvc --> Service: JobResponse\n{jobId, status: PENDING} + deactivate JobSvc + + == 4๋‹จ๊ณ„: ์‘๋‹ต ๋ฐ˜ํ™˜ == + + Service --> Controller: JobResponse\n{jobId, status: PENDING} + deactivate Service + + Controller --> Gateway: 202 Accepted\n{"jobId": "job-uuid-123",\n"status": "PENDING",\n"message": "AI๊ฐ€ ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค"} + deactivate Controller + + Gateway --> Client: 202 Accepted\nAI ๋ถ„์„ ์‹œ์ž‘ + deactivate Gateway + + note over Service, Kafka + **AI Service ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ** + - Kafka ๊ตฌ๋…: ai-job-topic + - ํŠธ๋ Œ๋“œ ๋ถ„์„ (์—…์ข…, ์ง€์—ญ ๊ธฐ๋ฐ˜) + - 3๊ฐ€์ง€ ์ถ”์ฒœ์•ˆ ์ƒ์„ฑ (์ €/์ค‘/๊ณ  ๋น„์šฉ) + - ๊ฒฐ๊ณผ: Redis์— ์ €์žฅ (TTL: 24์‹œ๊ฐ„) + - ์ƒ์„ธ: ai-ํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ถ”์ฒœ.puml ์ฐธ์กฐ + + **์ฒ˜๋ฆฌ ์‹œ๊ฐ„** + - ํ‰๊ท : 2๋ถ„ ์ด๋‚ด + - P95: 4๋ถ„ ์ด๋‚ด + - Timeout: 5๋ถ„ + + **๊ฒฐ๊ณผ ์กฐํšŒ** + - ํด๋ง ๋ฐฉ์‹: GET /api/jobs/{jobId}/status + - ๊ฐ„๊ฒฉ: 2์ดˆ, ์ตœ๋Œ€ 30์ดˆ + end note +end + +@enduml diff --git a/design/backend/sequence/inner/event-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml b/design/backend/sequence/inner/event-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml new file mode 100644 index 0000000..34187cd --- /dev/null +++ b/design/backend/sequence/inner/event-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ.puml @@ -0,0 +1,73 @@ +@startuml event-๋Œ€์‹œ๋ณด๋“œ์กฐํšŒ +!theme mono + +title Event Service - ๋Œ€์‹œ๋ณด๋“œ ์ด๋ฒคํŠธ ๋ชฉ๋ก (UFR-EVENT-010) + +actor Client +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> + +note over Controller: GET /api/events/dashboard +Controller -> Service: getDashboard(userId) +activate Service + +Service -> Cache: get("dashboard:" + userId) +activate Cache + +alt ์บ์‹œ ํžˆํŠธ + Cache --> Service: Dashboard data + Service --> Controller: DashboardResponse + +else ์บ์‹œ ๋ฏธ์Šค + Cache --> Service: null + deactivate Cache + + group parallel + Service -> Repo: findTopByStatusAndUserId(ACTIVE, userId, limit=5) + activate Repo + Repo -> DB: ์ง„ํ–‰์ค‘ ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ\n(์‚ฌ์šฉ์žID๋กœ ACTIVE ์ƒํƒœ ์ด๋ฒคํŠธ ์กฐํšŒ,\n์ฐธ์—ฌ์ž ์ˆ˜ ํ•จ๊ป˜ ์กฐํšŒ,\n์ƒ์„ฑ์ผ ๋‚ด๋ฆผ์ฐจ์ˆœ, ์ตœ๋Œ€ 5๊ฐœ) + activate DB + DB --> Repo: Active events + deactivate DB + Repo --> Service: List (active) + deactivate Repo + + Service -> Repo: findTopByStatusAndUserId(APPROVED, userId, limit=5) + activate Repo + Repo -> DB: ์˜ˆ์ • ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ\n(์‚ฌ์šฉ์žID๋กœ APPROVED ์ƒํƒœ ์ด๋ฒคํŠธ ์กฐํšŒ,\n์Šน์ธ์ผ ๋‚ด๋ฆผ์ฐจ์ˆœ, ์ตœ๋Œ€ 5๊ฐœ) + activate DB + DB --> Repo: Approved events + deactivate DB + Repo --> Service: List (approved) + deactivate Repo + + Service -> Repo: findTopByStatusAndUserId(COMPLETED, userId, limit=5) + activate Repo + Repo -> DB: ์ข…๋ฃŒ ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ\n(์‚ฌ์šฉ์žID๋กœ COMPLETED ์ƒํƒœ ์ด๋ฒคํŠธ ์กฐํšŒ,\n์ฐธ์—ฌ์ž ์ˆ˜ ํ•จ๊ป˜ ์กฐํšŒ,\n์ข…๋ฃŒ์ผ ๋‚ด๋ฆผ์ฐจ์ˆœ, ์ตœ๋Œ€ 5๊ฐœ) + activate DB + DB --> Repo: Completed events + deactivate DB + Repo --> Service: List (completed) + deactivate Repo + end + + Service -> Service: buildDashboardResponse(active, approved, completed) + note right: ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ:\n- ์ง„ํ–‰์ค‘: 5๊ฐœ\n- ์˜ˆ์ •: 5๊ฐœ\n- ์ข…๋ฃŒ: 5๊ฐœ\n๊ฐ ์นด๋“œ์— ๊ธฐ๋ณธ ํ†ต๊ณ„ ํฌํ•จ + + Service -> Cache: set("dashboard:" + userId,\ndashboard, TTL=1๋ถ„) + activate Cache + Cache --> Service: OK + deactivate Cache +end + +Service --> Controller: DashboardResponse\n{active: [...], approved: [...],\ncompleted: [...]} +deactivate Service + +Controller --> Client: 200 OK\n{active: [\n {eventId, title, period, status,\n participantCount, viewCount, ...}\n],\napproved: [...],\ncompleted: [...]} + +note over Controller, DB: ๋Œ€์‹œ๋ณด๋“œ ์นด๋“œ ์ •๋ณด:\n- ์ด๋ฒคํŠธ๋ช…\n- ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„\n- ์ง„ํ–‰ ์ƒํƒœ ๋ฑƒ์ง€\n- ๊ฐ„๋‹จํ•œ ํ†ต๊ณ„\n (์ฐธ์—ฌ์ž ์ˆ˜, ์กฐํšŒ์ˆ˜ ๋“ฑ)\n\n์„น์…˜๋‹น ์ตœ๋Œ€ 5๊ฐœ ํ‘œ์‹œ\n(์ตœ์‹  ์ˆœ) + +@enduml diff --git a/design/backend/sequence/inner/event-๋ชฉ๋ก์กฐํšŒ.puml b/design/backend/sequence/inner/event-๋ชฉ๋ก์กฐํšŒ.puml new file mode 100644 index 0000000..7be0f71 --- /dev/null +++ b/design/backend/sequence/inner/event-๋ชฉ๋ก์กฐํšŒ.puml @@ -0,0 +1,64 @@ +@startuml event-๋ชฉ๋ก์กฐํšŒ +!theme mono + +title Event Service - ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ (ํ•„ํ„ฐ/๊ฒ€์ƒ‰) (UFR-EVENT-070) + +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> + +note over Controller: GET /api/events?status={์ƒํƒœ}&keyword={๊ฒ€์ƒ‰์–ด}\n&page={ํŽ˜์ด์ง€}&size={ํฌ๊ธฐ} +Controller -> Service: ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ(์‚ฌ์šฉ์žID, ํ•„ํ„ฐ, ํŽ˜์ด์ง•) +activate Service + +Service -> Cache: ์บ์‹œ ์กฐํšŒ("events:" + ์‚ฌ์šฉ์žID + ":" + ํ•„ํ„ฐ + ":" + ํŽ˜์ด์ง€) +activate Cache + +alt ์บ์‹œ ํžˆํŠธ + Cache --> Service: ์ด๋ฒคํŠธ ๋ชฉ๋ก ๋ฐ์ดํ„ฐ + Service --> Controller: ์ด๋ฒคํŠธ ๋ชฉ๋ก ์‘๋‹ต + +else ์บ์‹œ ๋ฏธ์Šค + Cache --> Service: null + deactivate Cache + + Service -> Repo: ์‚ฌ์šฉ์ž๋ณ„ ํ•„ํ„ฐ๋ง ์ด๋ฒคํŠธ ์กฐํšŒ(์‚ฌ์šฉ์žID, ํ•„ํ„ฐ, ํŽ˜์ด์ง•) + activate Repo + + alt ํ•„ํ„ฐ ์žˆ์Œ (์ƒํƒœ๋ณ„) + Repo -> DB: ์‚ฌ์šฉ์ž๋ณ„ ํŠน์ • ์ƒํƒœ ์ด๋ฒคํŠธ ์กฐํšŒ\n(์ฐธ์—ฌ์ž ์ˆ˜ ํฌํ•จ, ์ƒ์„ฑ์ผ ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ,\nํŽ˜์ด์ง• ์ ์šฉ) + else ๊ฒ€์ƒ‰ ์žˆ์Œ (ํ‚ค์›Œ๋“œ) + Repo -> DB: ์‚ฌ์šฉ์ž๋ณ„ ์ด๋ฒคํŠธ ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰\n(์ œ๋ชฉ/์„ค๋ช…์—์„œ ๊ฒ€์ƒ‰, ์ฐธ์—ฌ์ž ์ˆ˜ ํฌํ•จ,\n์ƒ์„ฑ์ผ ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ, ํŽ˜์ด์ง• ์ ์šฉ) + else ํ•„ํ„ฐ ์—†์Œ (์ „์ฒด) + Repo -> DB: ์‚ฌ์šฉ์ž๋ณ„ ์ „์ฒด ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ\n(์ฐธ์—ฌ์ž ์ˆ˜ ํฌํ•จ, ์ƒ์„ฑ์ผ ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ,\nํŽ˜์ด์ง• ์ ์šฉ) + end + + activate DB + note right: ์ธ๋ฑ์Šค ํ™œ์šฉ:\n- ์‚ฌ์šฉ์žID\n- ์ƒํƒœ\n- ์ƒ์„ฑ์ผ์‹œ + DB --> Repo: ์ด๋ฒคํŠธ ๋ชฉ๋ก ๋ฐ ์ฐธ์—ฌ์ž ์ˆ˜ + deactivate DB + + Repo -> DB: ์ „์ฒด ์ด๋ฒคํŠธ ๊ฐœ์ˆ˜ ์กฐํšŒ\n(ํ•„ํ„ฐ ์กฐ๊ฑด ํฌํ•จ, ํŽ˜์ด์ง•์šฉ) + activate DB + DB --> Repo: ์ „์ฒด ๊ฐœ์ˆ˜ + deactivate DB + + Repo --> Service: ํŽ˜์ด์ง•๋œ ์ด๋ฒคํŠธ ๊ฒฐ๊ณผ + deactivate Repo + + Service -> Cache: ์บ์‹œ ์ €์žฅ("events:" + ์‚ฌ์šฉ์žID + ":" + ํ•„ํ„ฐ + ":" + ํŽ˜์ด์ง€,\nํŽ˜์ด์ง•๊ฒฐ๊ณผ, TTL=1๋ถ„) + activate Cache + Cache --> Service: OK + deactivate Cache +end + +Service --> Controller: ์ด๋ฒคํŠธ ๋ชฉ๋ก ์‘๋‹ต\n{์ด๋ฒคํŠธ๋ชฉ๋ก: [...], ์ „์ฒด๊ฐœ์ˆ˜,\n์ „์ฒดํŽ˜์ด์ง€์ˆ˜, ํ˜„์žฌํŽ˜์ด์ง€} +deactivate Service + +Controller --> Client: 200 OK\n{์ด๋ฒคํŠธ๋ชฉ๋ก: [\n {์ด๋ฒคํŠธID, ์ œ๋ชฉ, ๊ธฐ๊ฐ„, ์ƒํƒœ,\n ์ฐธ์—ฌ์ž์ˆ˜, ROI, ์ƒ์„ฑ์ผ์‹œ},\n ...\n],\n์ „์ฒด๊ฐœ์ˆ˜, ์ „์ฒดํŽ˜์ด์ง€์ˆ˜, ํ˜„์žฌํŽ˜์ด์ง€} + +note over Controller, DB: ํ•„ํ„ฐ ์˜ต์…˜:\n- ์ƒํƒœ: ์ž„์‹œ์ €์žฅ, ์ง„ํ–‰์ค‘, ์™„๋ฃŒ\n- ๊ธฐ๊ฐ„: ์ตœ๊ทผ 1๊ฐœ์›”/3๊ฐœ์›”/6๊ฐœ์›”/1๋…„\n- ์ •๋ ฌ: ์ตœ์‹ ์ˆœ, ์ฐธ์—ฌ์ž ๋งŽ์€ ์ˆœ,\n ROI ๋†’์€ ์ˆœ\n\nํŽ˜์ด์ง€๋„ค์ด์…˜:\n- ๊ธฐ๋ณธ 20๊ฐœ/ํŽ˜์ด์ง€\n- ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜ + +@enduml diff --git a/design/backend/sequence/inner/event-๋ชฉ์ ์„ ํƒ.puml b/design/backend/sequence/inner/event-๋ชฉ์ ์„ ํƒ.puml new file mode 100644 index 0000000..2c65eaa --- /dev/null +++ b/design/backend/sequence/inner/event-๋ชฉ์ ์„ ํƒ.puml @@ -0,0 +1,110 @@ +@startuml event-๋ชฉ์ ์„ ํƒ +!theme mono + +title Event Service - ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ ๋ฐ ์ €์žฅ (UFR-EVENT-020) + +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> +participant "Kafka Producer" as Kafka <> +actor Client + +note over Controller, DB +**UFR-EVENT-020: ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ ๋ฐ ์ €์žฅ** +- ๋ชฉ์  ์„ ํƒ: ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜, ์žฌ๋ฐฉ๋ฌธ ์œ ๋„, ๋งค์ถœ ์ฆ๋Œ€, ์ธ์ง€๋„ ํ–ฅ์ƒ +- Redis ์บ์‹œ ์‚ฌ์šฉ (TTL: 30๋ถ„) +- Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ (EventDraftCreated) +- ์‚ฌ์šฉ์ž ๋ฐ ๋งค์žฅ ์ •๋ณด๋Š” User Service์—์„œ ์กฐํšŒ ํ›„ ์ „๋‹ฌ๋จ +end note + +Client -> Controller: POST /api/events/purposes\n{"userId": 123,\n"objective": "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜",\n"storeName": "๋ง›์žˆ๋Š”์ง‘",\n"industry": "์Œ์‹์ ",\n"address": "์„œ์šธ์‹œ ๊ฐ•๋‚จ๊ตฌ"} +activate Controller + +Controller -> Controller: ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ\n(ํ•„์ˆ˜ ํ•„๋“œ, ๋ชฉ์  ์œ ํšจ์„ฑ ํ™•์ธ) + +Controller -> Service: createEventDraft(userId, objective, storeInfo) +activate Service + +== 1๋‹จ๊ณ„: Redis ์บ์‹œ ํ™•์ธ == + +Service -> Cache: ์บ์‹œ ์กฐํšŒ\nKey: draft:event:{userId}\n(๊ธฐ์กด ์ž‘์„ฑ ์ค‘์ธ ์ด๋ฒคํŠธ ํ™•์ธ) +activate Cache +Cache --> Service: null (์บ์‹œ ๋ฏธ์Šค) +deactivate Cache + +== 2๋‹จ๊ณ„: ๋ชฉ์  ์œ ํšจ์„ฑ ๊ฒ€์ฆ == + +Service -> Service: ๋ชฉ์  ์œ ํšจ์„ฑ ๊ฒ€์ฆ\n- ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜\n- ์žฌ๋ฐฉ๋ฌธ ์œ ๋„\n- ๋งค์ถœ ์ฆ๋Œ€\n- ์ธ์ง€๋„ ํ–ฅ์ƒ + +Service -> Service: ๋งค์žฅ ์ •๋ณด ์œ ํšจ์„ฑ ๊ฒ€์ฆ\n(๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ) + +== 3๋‹จ๊ณ„: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์ €์žฅ == + +Service -> Repo: save(eventDraft) +activate Repo +Repo -> DB: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์ €์žฅ\n(์‚ฌ์šฉ์žID, ๋ชฉ์ , ๋งค์žฅ๋ช…,\n์—…์ข…, ์ฃผ์†Œ, ์ƒํƒœ=DRAFT,\n์ƒ์„ฑ์ผ์‹œ)\n์ €์žฅ ํ›„ ์ด๋ฒคํŠธ์ดˆ์•ˆID ๋ฐ˜ํ™˜ +activate DB +DB --> Repo: ์ƒ์„ฑ๋œ ์ด๋ฒคํŠธ์ดˆ์•ˆID +deactivate DB +Repo --> Service: EventDraft ์—”ํ‹ฐํ‹ฐ\n(eventDraftId ํฌํ•จ) +deactivate Repo + +== 4๋‹จ๊ณ„: Redis ์บ์‹œ ์ €์žฅ == + +Service -> Cache: ์บ์‹œ ์ €์žฅ\nKey: draft:event:{eventDraftId}\nValue: {๋ชฉ์ , ๋งค์žฅ์ •๋ณด, ์ƒํƒœ}\nTTL: 24์‹œ๊ฐ„ +activate Cache +Cache --> Service: ์ €์žฅ ์™„๋ฃŒ +deactivate Cache + +== 5๋‹จ๊ณ„: Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ == + +Service -> Kafka: ์ด๋ฒคํŠธ ๋ฐœํ–‰\nTopic: event-topic\nEvent: EventDraftCreated\nPayload: {eventDraftId,\nuserId, objective,\ncreatedAt} +activate Kafka +note right of Kafka +**Kafka Event Topic** +- Topic: event-topic +- Event: EventDraftCreated +- ๋ชฉ์  ์„ ํƒ ์‹œ ๋ฐœํ–‰ + +**๊ตฌ๋…์ž** +- Analytics Service (์„ ํƒ์ ) + +**์ฐธ๊ณ ** +- EventCreated๋Š” + ์ตœ์ข… ์Šน์ธ ์‹œ ๋ฐœํ–‰ +end note +Kafka --> Service: ACK (๋ฐœํ–‰ ํ™•์ธ) +deactivate Kafka + +== 6๋‹จ๊ณ„: ์‘๋‹ต ๋ฐ˜ํ™˜ == + +Service -> Service: ์‘๋‹ต DTO ์ƒ์„ฑ +Service --> Controller: EventDraftResponse\n{eventDraftId, objective,\nstoreName, status=DRAFT} +deactivate Service + +Controller --> Client: 200 OK\n{"eventDraftId": "draft-123",\n"objective": "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜",\n"storeName": "๋ง›์žˆ๋Š”์ง‘",\n"status": "DRAFT"} +deactivate Controller + +note over Controller, Kafka +**์บ์‹œ ์ „๋žต** +- Key: draft:event:{eventDraftId} +- TTL: 24์‹œ๊ฐ„ +- ์บ์‹œ ํžˆํŠธ ์‹œ: DB ์กฐํšŒ ์ƒ๋žต, ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + +**์ด๋ฒคํŠธ ๋ฐœํ–‰ ์ „๋žต** +- EventDraftCreated: ๋ชฉ์  ์„ ํƒ ์‹œ ๋ฐœํ–‰ (Analytics Service ์„ ํƒ์  ๊ตฌ๋…) +- EventCreated: ์ตœ์ข… ์Šน์ธ ์‹œ ๋ฐœํ–‰ (ํ†ต๊ณ„ ์ดˆ๊ธฐํ™” ์‹œ์ž‘) + +**์„ฑ๋Šฅ ๋ชฉํ‘œ** +- ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„: 0.3์ดˆ ์ด๋‚ด +- P95 ์‘๋‹ต ์‹œ๊ฐ„: 0.5์ดˆ ์ด๋‚ด +- Redis ์บ์‹œ ์กฐํšŒ: 0.05์ดˆ ์ด๋‚ด + +**์—๋Ÿฌ ์ฝ”๋“œ** +- EVENT_001: ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ชฉ์  +- EVENT_002: ๋งค์žฅ ์ •๋ณด ๋ˆ„๋ฝ +end note + +@enduml diff --git a/design/backend/sequence/inner/event-์ƒ์„ธ์กฐํšŒ.puml b/design/backend/sequence/inner/event-์ƒ์„ธ์กฐํšŒ.puml new file mode 100644 index 0000000..4e94e8e --- /dev/null +++ b/design/backend/sequence/inner/event-์ƒ์„ธ์กฐํšŒ.puml @@ -0,0 +1,54 @@ +@startuml event-์ƒ์„ธ์กฐํšŒ +!theme mono + +title Event Service - ์ด๋ฒคํŠธ ์ƒ์„ธ ์กฐํšŒ (UFR-EVENT-060) + +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> + +note over Controller: GET /api/events/{id} +Controller -> Service: getEventDetail(eventId, userId) +activate Service + +Service -> Cache: get("event:" + eventId) +activate Cache + +alt ์บ์‹œ ํžˆํŠธ + Cache --> Service: Event data + Service -> Service: validateAccess(userId, event) + note right: ์‚ฌ์šฉ์ž ๊ถŒํ•œ ๊ฒ€์ฆ + Service --> Controller: EventDetailResponse + +else ์บ์‹œ ๋ฏธ์Šค + Cache --> Service: null + deactivate Cache + + Service -> Repo: findById(eventId) + activate Repo + Repo -> DB: ์ด๋ฒคํŠธ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ\n(์ด๋ฒคํŠธID๋กœ ์ด๋ฒคํŠธ ์ •๋ณด,\n๊ฒฝํ’ˆ ์ •๋ณด, ๋ฐฐํฌ ์ด๋ ฅ์„\nJOINํ•˜์—ฌ ํ•จ๊ป˜ ์กฐํšŒ) + activate DB + note right: JOIN์œผ๋กœ\n๊ฒฝํ’ˆ ์ •๋ณด ๋ฐ\n๋ฐฐํฌ ์ด๋ ฅ ์กฐํšŒ + DB --> Repo: Event with prizes and distributions + deactivate DB + Repo --> Service: Event entity (with relations) + deactivate Repo + + Service -> Service: validateAccess(userId, event) + + Service -> Cache: set("event:" + eventId, event, TTL=5๋ถ„) + activate Cache + Cache --> Service: OK + deactivate Cache +end + +Service --> Controller: EventDetailResponse\n{eventId, title, objective,\nprizes, period, status,\nchannels, distributionStatus,\ncreatedAt, publishedAt} +deactivate Service + +Controller --> Client: 200 OK\n{event: {...},\nprizes: [...],\ndistributionStatus: {...}} + +note over Controller, DB: ์ƒ์„ธ ์ •๋ณด ํฌํ•จ:\n- ๊ธฐ๋ณธ ์ •๋ณด (์ œ๋ชฉ, ๋ชฉ์ , ๊ธฐ๊ฐ„, ์ƒํƒœ)\n- ๊ฒฝํ’ˆ ์ •๋ณด\n- ์ฐธ์—ฌ ๋ฐฉ๋ฒ•\n- ๋ฐฐํฌ ์ฑ„๋„ ํ˜„ํ™ฉ\n- ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ (Analytics Service)\n\nAnalytics ํ†ต๊ณ„๋Š”\n๋ณ„๋„ API ํ˜ธ์ถœ + +@enduml diff --git a/design/backend/sequence/inner/event-์ตœ์ข…์Šน์ธ๋ฐ๋ฐฐํฌ.puml b/design/backend/sequence/inner/event-์ตœ์ข…์Šน์ธ๋ฐ๋ฐฐํฌ.puml new file mode 100644 index 0000000..725af86 --- /dev/null +++ b/design/backend/sequence/inner/event-์ตœ์ข…์Šน์ธ๋ฐ๋ฐฐํฌ.puml @@ -0,0 +1,157 @@ +@startuml event-์ตœ์ข…์Šน์ธ๋ฐ๋ฐฐํฌ +!theme mono + +title Event Service - ์ตœ์ข… ์Šน์ธ ๋ฐ Distribution Service ๋™๊ธฐ ํ˜ธ์ถœ (UFR-EVENT-050) + +actor Client +participant "API Gateway" as Gateway +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> +participant "Distribution Service" as DistSvc <> +participant "Kafka Producer" as Kafka <> + +note over Controller, Kafka +**UFR-EVENT-050: ์ด๋ฒคํŠธ ์ตœ์ข… ์Šน์ธ ๋ฐ ๋ฐฐํฌ** +- ์ด๋ฒคํŠธ ์ค€๋น„ ์ƒํƒœ ๊ฒ€์ฆ +- ์ด๋ฒคํŠธ ์Šน์ธ ๋ฐ Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ +- Distribution Service ๋™๊ธฐ ํ˜ธ์ถœ (๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ) +- ์ด๋ฒคํŠธ ์ƒํƒœ๋ฅผ ACTIVE๋กœ ๋ณ€๊ฒฝ +end note + +Client -> Gateway: POST /api/events/{eventDraftId}/publish\n{"userId": 123,\n"selectedChannels": [\n "์šฐ๋ฆฌ๋™๋„คTV",\n "์ง€๋‹ˆTV",\n "Instagram"\n]} +activate Gateway + +Gateway -> Controller: POST /api/events/{eventDraftId}/publish +activate Controller + +Controller -> Controller: ์š”์ฒญ ๊ฒ€์ฆ\n(ํ•„์ˆ˜ ํ•„๋“œ, ์ฑ„๋„ ์œ ํšจ์„ฑ) + +Controller -> Service: publishEvent(eventDraftId, userId, selectedChannels) +activate Service + +== 1๋‹จ๊ณ„: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์กฐํšŒ ๋ฐ ๊ฒ€์ฆ == + +Service -> Repo: findById(eventDraftId) +activate Repo +Repo -> DB: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์กฐํšŒ\n(์ดˆ์•ˆID๋กœ ์กฐํšŒ) +activate DB +DB --> Repo: EventDraft ์—”ํ‹ฐํ‹ฐ\n{๋ชฉ์ , ์ถ”์ฒœ์•ˆ, ์ฝ˜ํ…์ธ , ์ƒํƒœ} +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +Service -> Service: validateOwnership(userId, eventDraft) +note right +์†Œ์œ ๊ถŒ ๊ฒ€์ฆ: +- ์‚ฌ์šฉ์žID์™€ ์ดˆ์•ˆ ์†Œ์œ ์ž ์ผ์น˜ ํ™•์ธ +- ๊ถŒํ•œ ์—†์œผ๋ฉด 403 Forbidden +end note + +Service -> Service: validatePublishReady() +note right +๋ฐœํ–‰ ์ค€๋น„ ๊ฒ€์ฆ: +- ๋ชฉ์  ์„ ํƒ ์™„๋ฃŒ +- AI ์ถ”์ฒœ ์„ ํƒ ์™„๋ฃŒ +- ์ฝ˜ํ…์ธ  ์„ ํƒ ์™„๋ฃŒ +- ๋ฐฐํฌ ์ฑ„๋„ ์ตœ์†Œ 1๊ฐœ ์„ ํƒ +end note + +== 2๋‹จ๊ณ„: ์ด๋ฒคํŠธ ์Šน์ธ == + +Service -> Repo: updateStatus(eventDraftId, APPROVED) +activate Repo +Repo -> DB: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์ƒํƒœ ์—…๋ฐ์ดํŠธ\n(์ƒํƒœ๋ฅผ APPROVED๋กœ,\n์Šน์ธ์ผ์‹œ๋ฅผ ํ˜„์žฌ ์‹œ๊ฐ์œผ๋กœ ์ €์žฅ) +activate DB +DB --> Repo: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +== 3๋‹จ๊ณ„: Kafka ์ด๋ฒคํŠธ ๋ฐœํ–‰ == + +Service -> Kafka: ์ด๋ฒคํŠธ ๋ฐœํ–‰\nTopic: event-topic\nPayload: {eventId, userId, title,\nobjective, createdAt} +activate Kafka +note right +Kafka Event Topic: +- Topic: event-topic +- Consumer: Analytics Service +- Event Type: EventCreated +end note +Kafka --> Service: ACK (๋ฐœํ–‰ ํ™•์ธ) +deactivate Kafka + +== 4๋‹จ๊ณ„: Distribution Service ๋™๊ธฐ ํ˜ธ์ถœ == + +Service -> DistSvc: POST /api/distribution/distribute\n{"eventId": 123,\n"channels": [...],\n"content": {...}} +activate DistSvc +note right +๋™๊ธฐ ํ˜ธ์ถœ (Circuit Breaker ์ ์šฉ): +- Timeout: 70์ดˆ +- ๋‹ค์ค‘ ์ฑ„๋„ ๋ณ‘๋ ฌ ๋ฐฐํฌ +- Failure Rate: 50% ์ดˆ๊ณผ ์‹œ OPEN +end note + +DistSvc -> DistSvc: distributeToChannels(eventId, channels) +note right +๋‹ค์ค‘ ์ฑ„๋„ ๋ณ‘๋ ฌ ๋ฐฐํฌ: +- ์šฐ๋ฆฌ๋™๋„คTV +- ๋ง๊ณ ๋น„์ฆˆ (์Œ์„ฑ ์•ˆ๋‚ด) +- ์ง€๋‹ˆTV +- Instagram +- Naver Blog +- Kakao Channel +end note + +DistSvc --> Service: DistributionResponse\n{"distributionId": "dist-123",\n"channelResults": [\n {"channel": "์šฐ๋ฆฌ๋™๋„คTV", "status": "SUCCESS"},\n {"channel": "์ง€๋‹ˆTV", "status": "SUCCESS"},\n {"channel": "Instagram", "status": "SUCCESS"}\n]} +deactivate DistSvc + +== 5๋‹จ๊ณ„: ์ด๋ฒคํŠธ ํ™œ์„ฑํ™” == + +Service -> Repo: updateStatus(eventDraftId, ACTIVE) +activate Repo +Repo -> DB: ์ด๋ฒคํŠธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ\n(์ƒํƒœ๋ฅผ ACTIVE๋กœ,\n๋ฐฐํฌ์ผ์‹œ๋ฅผ ํ˜„์žฌ ์‹œ๊ฐ์œผ๋กœ ์ €์žฅ) +activate DB +DB --> Repo: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ +deactivate DB +Repo --> Service: Event entity +deactivate Repo + +== 6๋‹จ๊ณ„: ์บ์‹œ ๋ฌดํšจํ™” == + +Service -> Cache: ์บ์‹œ ์‚ญ์ œ\nKey: draft:event:{eventDraftId} +activate Cache +note right +Redis ์บ์‹œ ๋ฌดํšจํ™”: +- ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์บ์‹œ ์‚ญ์ œ +- ํ™œ์„ฑํ™” ์™„๋ฃŒ ํ›„ ๋ถˆํ•„์š” +end note +Cache --> Service: ์‚ญ์ œ ์™„๋ฃŒ +deactivate Cache + +== 7๋‹จ๊ณ„: ์‘๋‹ต ๋ฐ˜ํ™˜ == + +Service --> Controller: PublishResponse\n{eventId, status: ACTIVE,\ndistributionResults} +deactivate Service + +Controller --> Gateway: 200 OK\n{"eventId": 123,\n"status": "ACTIVE",\n"distributionResults": [...]} +deactivate Controller + +Gateway --> Client: 200 OK\n์ด๋ฒคํŠธ ๋ฐฐํฌ ์™„๋ฃŒ +deactivate Gateway + +note over Client, Kafka +**๋ฐฐํฌ ์™„๋ฃŒ ํ›„ ์ฒ˜๋ฆฌ** +- Distribution Service๋Š” ๋ฐฐํฌ ์™„๋ฃŒ ํ›„ Kafka์—\n DistributionCompleted ์ด๋ฒคํŠธ ๋ฐœํ–‰ +- Analytics Service๊ฐ€ ๊ตฌ๋…ํ•˜์—ฌ ์ดˆ๊ธฐ ํ†ต๊ณ„ ์ƒ์„ฑ +- ์ด๋ฒคํŠธ ์ƒํƒœ: ACTIVE (์ฐธ์—ฌ์ž ์ ‘์ˆ˜ ์‹œ์ž‘) + +**์„ฑ๋Šฅ ๋ชฉํ‘œ** +- ์‘๋‹ต ์‹œ๊ฐ„: 60์ดˆ ์ด๋‚ด (Distribution Service ํฌํ•จ) +- Distribution Service ํƒ€์ž„์•„์›ƒ: 70์ดˆ +- ์ฑ„๋„๋ณ„ ๋ฐฐํฌ: ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์ตœ์ ํ™” +end note + +@enduml diff --git a/design/backend/sequence/inner/event-์ถ”์ฒœ๊ฒฐ๊ณผ์กฐํšŒ.puml b/design/backend/sequence/inner/event-์ถ”์ฒœ๊ฒฐ๊ณผ์กฐํšŒ.puml new file mode 100644 index 0000000..c69425a --- /dev/null +++ b/design/backend/sequence/inner/event-์ถ”์ฒœ๊ฒฐ๊ณผ์กฐํšŒ.puml @@ -0,0 +1,140 @@ +@startuml event-์ถ”์ฒœ๊ฒฐ๊ณผ์กฐํšŒ +!theme mono + +title Event Service - AI ์ถ”์ฒœ ๊ฒฐ๊ณผ ํด๋ง ์กฐํšŒ + +actor Client +participant "API Gateway" as Gateway +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "JobManager" as JobMgr <> +participant "Redis Cache" as Cache <> + +note over Controller, Cache +**ํด๋ง ๋ฐฉ์‹ Job ์ƒํƒœ ์กฐํšŒ** +- ์ตœ๋Œ€ 30์ดˆ ๋™์•ˆ ํด๋ง (2์ดˆ ๊ฐ„๊ฒฉ) +- Job ์ƒํƒœ: PENDING โ†’ PROCESSING โ†’ COMPLETED +- AI ์ถ”์ฒœ ๊ฒฐ๊ณผ: Redis์— ์ €์žฅ (TTL: 24์‹œ๊ฐ„) +end note + +Client -> Gateway: GET /api/events/jobs/{jobId}/status +activate Gateway + +Gateway -> Controller: GET /api/events/jobs/{jobId}/status +activate Controller + +Controller -> Service: getJobStatus(jobId) +activate Service + +Service -> JobMgr: getJobStatus(jobId) +activate JobMgr + +JobMgr -> Cache: Job ์ƒํƒœ ์กฐํšŒ\nKey: job:{jobId} +activate Cache + +alt Job ๋ฐ์ดํ„ฐ ์กด์žฌ + Cache --> JobMgr: Job ๋ฐ์ดํ„ฐ\n{status, eventDraftId,\ntype, createdAt} + deactivate Cache + + alt status = COMPLETED + JobMgr -> Cache: AI ์ถ”์ฒœ ๊ฒฐ๊ณผ ์กฐํšŒ\nKey: ai:recommendation:{eventDraftId} + activate Cache + Cache --> JobMgr: AI ์ถ”์ฒœ ๊ฒฐ๊ณผ\n{ํŠธ๋ Œ๋“œ๋ถ„์„, 3๊ฐ€์ง€์ถ”์ฒœ์•ˆ} + deactivate Cache + + JobMgr --> Service: JobStatusResponse\n{jobId, status: COMPLETED,\nrecommendations: {...}} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: COMPLETED, recommendations} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "COMPLETED",\n"recommendations": [\n {"title": "์ €๋น„์šฉ ์ถ”์ฒœ์•ˆ",\n "prize": "์ปคํ”ผ์ฟ ํฐ",\n "method": "QR์ฝ”๋“œ ์Šค์บ”",\n "cost": "50๋งŒ์›",\n "roi": "150%"},\n {"title": "์ค‘๋น„์šฉ ์ถ”์ฒœ์•ˆ",\n "prize": "์ƒํ’ˆ๊ถŒ",\n "method": "SNS ๊ณต์œ ",\n "cost": "100๋งŒ์›",\n "roi": "200%"},\n {"title": "๊ณ ๋น„์šฉ ์ถ”์ฒœ์•ˆ",\n "prize": "๊ฒฝํ’ˆ ์ถ”์ฒจ",\n "method": "์„ค๋ฌธ ์ฐธ์—ฌ",\n "cost": "200๋งŒ์›",\n "roi": "300%"}\n]} + deactivate Controller + + Gateway --> Client: 200 OK\nAI ์ถ”์ฒœ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ + deactivate Gateway + + note right of Client + **ํ”„๋ก ํŠธ์—”๋“œ ์ฒ˜๋ฆฌ** + - 3๊ฐ€์ง€ ์ถ”์ฒœ์•ˆ ์นด๋“œ ํ‘œ์‹œ + - ์‚ฌ์šฉ์ž๊ฐ€ ์ถ”์ฒœ์•ˆ ์„ ํƒ + - ํŠธ๋ Œ๋“œ ๋ถ„์„ ์ •๋ณด ํ‘œ์‹œ + end note + + else status = PROCESSING ๋˜๋Š” PENDING + JobMgr --> Service: JobStatusResponse\n{jobId, status: PROCESSING} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: PROCESSING} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "PROCESSING",\n"message": "AI๊ฐ€ ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค"} + deactivate Controller + + Gateway --> Client: 200 OK\n์ง„ํ–‰ ์ค‘ ์ƒํƒœ + deactivate Gateway + + note right of Client + **ํด๋ง ์žฌ์‹œ๋„** + - 2์ดˆ ํ›„ ์žฌ์š”์ฒญ + - ์ตœ๋Œ€ 30์ดˆ (15ํšŒ) + end note + + else status = FAILED + JobMgr -> Cache: ์—๋Ÿฌ ์ •๋ณด ์กฐํšŒ\nKey: job:{jobId}:error + activate Cache + Cache --> JobMgr: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + deactivate Cache + + JobMgr --> Service: JobStatusResponse\n{jobId, status: FAILED, error} + deactivate JobMgr + + Service --> Controller: JobStatusResponse\n{status: FAILED, error} + deactivate Service + + Controller --> Gateway: 200 OK\n{"status": "FAILED",\n"error": "AI ๋ถ„์„ ์‹คํŒจ",\n"message": "๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”"} + deactivate Controller + + Gateway --> Client: 200 OK\n์‹คํŒจ ์ƒํƒœ + deactivate Gateway + + note right of Client + **์‹คํŒจ ์ฒ˜๋ฆฌ** + - ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + - "๋‹ค์‹œ ๋ถ„์„" ๋ฒ„ํŠผ ์ œ๊ณต + end note + end + +else Job ๋ฐ์ดํ„ฐ ์—†์Œ + Cache --> JobMgr: null (์บ์‹œ ๋ฏธ์Šค) + deactivate Cache + + JobMgr --> Service: throw NotFoundException\n("Job์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + deactivate JobMgr + + Service --> Controller: NotFoundException + deactivate Service + + Controller --> Gateway: 404 Not Found\n{"code": "JOB_001",\n"message": "Job์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"} + deactivate Controller + + Gateway --> Client: 404 Not Found + deactivate Gateway +end + +note over Controller, Cache +**ํด๋ง ์ „๋žต** +- ๊ฐ„๊ฒฉ: 2์ดˆ +- ์ตœ๋Œ€ ์‹œ๊ฐ„: 30์ดˆ (15ํšŒ) +- Timeout ์‹œ: ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ + "๋‹ค์‹œ ๋ถ„์„" ์˜ต์…˜ + +**Redis ์บ์‹œ** +- Job ์ƒํƒœ: TTL 1์‹œ๊ฐ„ +- AI ์ถ”์ฒœ ๊ฒฐ๊ณผ: TTL 24์‹œ๊ฐ„ + +**์„ฑ๋Šฅ ๋ชฉํ‘œ** +- ํ‰๊ท  AI ๋ถ„์„ ์‹œ๊ฐ„: 2๋ถ„ ์ด๋‚ด +- P95 AI ๋ถ„์„ ์‹œ๊ฐ„: 4๋ถ„ ์ด๋‚ด +end note + +@enduml diff --git a/design/backend/sequence/inner/event-์ถ”์ฒœ์•ˆ์„ ํƒ.puml b/design/backend/sequence/inner/event-์ถ”์ฒœ์•ˆ์„ ํƒ.puml new file mode 100644 index 0000000..295bdea --- /dev/null +++ b/design/backend/sequence/inner/event-์ถ”์ฒœ์•ˆ์„ ํƒ.puml @@ -0,0 +1,116 @@ +@startuml event-์ถ”์ฒœ์•ˆ์„ ํƒ +!theme mono + +title Event Service - ์„ ํƒํ•œ AI ์ถ”์ฒœ์•ˆ ์ €์žฅ (UFR-EVENT-040) + +actor Client +participant "API Gateway" as Gateway +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> + +note over Controller, Cache +**UFR-EVENT-040: AI ์ถ”์ฒœ์•ˆ ์„ ํƒ ๋ฐ ์ €์žฅ** +- ์‚ฌ์šฉ์ž๊ฐ€ 3๊ฐ€์ง€ ์ถ”์ฒœ์•ˆ ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒ +- ์„ ํƒ๋œ ์ถ”์ฒœ์•ˆ์„ ์ด๋ฒคํŠธ ์ดˆ์•ˆ์— ์ ์šฉ +- Redis ์บ์‹œ์—์„œ AI ์ถ”์ฒœ ๊ฒฐ๊ณผ ์‚ญ์ œ +end note + +Client -> Gateway: PUT /api/events/drafts/{eventDraftId}/recommendation\n{"userId": 123,\n"selectedIndex": 1,\n"recommendation": {...}} +activate Gateway + +Gateway -> Controller: PUT /api/events/drafts/{eventDraftId}/recommendation +activate Controller + +Controller -> Controller: ์š”์ฒญ ๊ฒ€์ฆ\n(ํ•„์ˆ˜ ํ•„๋“œ, ์ถ”์ฒœ์•ˆ ์œ ํšจ์„ฑ) + +Controller -> Service: updateEventRecommendation(eventDraftId, userId,\nselectedRecommendation) +activate Service + +== 1๋‹จ๊ณ„: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์กฐํšŒ ๋ฐ ๊ฒ€์ฆ == + +Service -> Repo: findById(eventDraftId) +activate Repo +Repo -> DB: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์กฐํšŒ\n(์ดˆ์•ˆID๋กœ ์กฐํšŒ) +activate DB +DB --> Repo: EventDraft ์—”ํ‹ฐํ‹ฐ\n{๋ชฉ์ , ๋งค์žฅ์ •๋ณด, ์ƒํƒœ} +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +Service -> Service: validateOwnership(userId, eventDraft) +note right +์†Œ์œ ๊ถŒ ๊ฒ€์ฆ: +- ์‚ฌ์šฉ์žID์™€ ์ดˆ์•ˆ ์†Œ์œ ์ž ์ผ์น˜ ํ™•์ธ +- ๊ถŒํ•œ ์—†์œผ๋ฉด 403 Forbidden +end note + +Service -> Service: validateRecommendation(selectedRecommendation) +note right +์ถ”์ฒœ์•ˆ ์œ ํšจ์„ฑ ๊ฒ€์ฆ: +- ํ•„์ˆ˜ ํ•„๋“œ ์กด์žฌ ์—ฌ๋ถ€ +- ๋น„์šฉ/ROI ๊ฐ’ ํƒ€๋‹น์„ฑ +end note + +== 2๋‹จ๊ณ„: ์ถ”์ฒœ์•ˆ ์ ์šฉ == + +Service -> Service: applyRecommendation(eventDraft, selectedRecommendation) +note right +์ถ”์ฒœ์•ˆ ์ ์šฉ: +- ์ด๋ฒคํŠธ ์ œ๋ชฉ +- ๊ฒฝํ’ˆ ์ •๋ณด +- ์ฐธ์—ฌ ๋ฐฉ๋ฒ• +- ์˜ˆ์ƒ ๋น„์šฉ +- ์˜ˆ์ƒ ROI +- ํ™๋ณด ๋ฌธ๊ตฌ +end note + +== 3๋‹จ๊ณ„: DB ์ €์žฅ == + +Service -> Repo: update(eventDraft) +activate Repo +Repo -> DB: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์—…๋ฐ์ดํŠธ\n(์„ ํƒ๋œ ์ถ”์ฒœ์•ˆ ์ •๋ณด ์ €์žฅ:\n์ œ๋ชฉ, ๊ฒฝํ’ˆ, ์ฐธ์—ฌ๋ฐฉ๋ฒ•,\n์˜ˆ์ƒ๋น„์šฉ, ์˜ˆ์ƒROI,\n์ˆ˜์ •์ผ์‹œ) +activate DB +DB --> Repo: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +== 4๋‹จ๊ณ„: ์บ์‹œ ๋ฌดํšจํ™” == + +Service -> Cache: ์บ์‹œ ์‚ญ์ œ\nKey: ai:recommendation:{eventDraftId} +activate Cache +note right +Redis ์บ์‹œ ๋ฌดํšจํ™”: +- AI ์ถ”์ฒœ ๊ฒฐ๊ณผ ์‚ญ์ œ +- ์„ ํƒ ์™„๋ฃŒ ํ›„ ๋ถˆํ•„์š” +end note +Cache --> Service: ์‚ญ์ œ ์™„๋ฃŒ +deactivate Cache + +== 5๋‹จ๊ณ„: ์‘๋‹ต ๋ฐ˜ํ™˜ == + +Service --> Controller: EventRecommendationResponse\n{eventDraftId, selectedRecommendation} +deactivate Service + +Controller --> Gateway: 200 OK\n{"eventDraftId": 123,\n"status": "์ถ”์ฒœ์•ˆ ์„ ํƒ ์™„๋ฃŒ",\n"selectedRecommendation": {...}} +deactivate Controller + +Gateway --> Client: 200 OK\n์ถ”์ฒœ์•ˆ ์„ ํƒ ์™„๋ฃŒ +deactivate Gateway + +note over Client, Cache +**์ €์žฅ ๋‚ด์šฉ** +- ์ตœ์ข… ์„ ํƒ๋œ ์ถ”์ฒœ์•ˆ๋งŒ Event DB์— ์ €์žฅ +- Redis์— ์ €์žฅ๋œ 3๊ฐ€์ง€ ์ถ”์ฒœ์•ˆ์€ ์„ ํƒ ํ›„ ์‚ญ์ œ +- ๋‹ค์Œ ๋‹จ๊ณ„: ์ฝ˜ํ…์ธ  ์ƒ์„ฑ (์ด๋ฏธ์ง€ ์„ ํƒ) + +**์„ฑ๋Šฅ ๋ชฉํ‘œ** +- ์‘๋‹ต ์‹œ๊ฐ„: 0.5์ดˆ ์ด๋‚ด +- DB ์—…๋ฐ์ดํŠธ: 0.1์ดˆ +- ์บ์‹œ ์‚ญ์ œ: 0.01์ดˆ +end note + +@enduml diff --git a/design/backend/sequence/inner/event-์ฝ˜ํ…์ธ ์„ ํƒ.puml b/design/backend/sequence/inner/event-์ฝ˜ํ…์ธ ์„ ํƒ.puml new file mode 100644 index 0000000..464d55a --- /dev/null +++ b/design/backend/sequence/inner/event-์ฝ˜ํ…์ธ ์„ ํƒ.puml @@ -0,0 +1,118 @@ +@startuml event-์ฝ˜ํ…์ธ ์„ ํƒ +!theme mono + +title Content Service - ์„ ํƒํ•œ ์ฝ˜ํ…์ธ  ์ €์žฅ (UFR-CONT-020) + +actor Client +participant "API Gateway" as Gateway +participant "ContentController" as Controller <> +participant "ContentService" as Service <> +participant "EventRepository" as Repo <> +participant "Redis Cache" as Cache <> +database "Event DB" as DB <> + +note over Controller, Cache +**UFR-CONT-020: ์ฝ˜ํ…์ธ  ์„ ํƒ ๋ฐ ํŽธ์ง‘ ์ €์žฅ** +- ์‚ฌ์šฉ์ž๊ฐ€ 3๊ฐ€์ง€ ์ด๋ฏธ์ง€ ์Šคํƒ€์ผ ์ค‘ ํ•˜๋‚˜ ์„ ํƒ +- ์„ ํƒ๋œ ์ด๋ฏธ์ง€์— ํ…์ŠคํŠธ/์ƒ‰์ƒ ํŽธ์ง‘ ์ ์šฉ +- ํŽธ์ง‘๋œ ์ฝ˜ํ…์ธ ๋ฅผ ์ด๋ฒคํŠธ ์ดˆ์•ˆ์— ์ €์žฅ +end note + +Client -> Gateway: PUT /api/content/{eventDraftId}/select\n{"userId": 123,\n"selectedImageUrl": "https://cdn.../fancy.png",\n"editedContent": {...}} +activate Gateway + +Gateway -> Controller: PUT /api/content/{eventDraftId}/select +activate Controller + +Controller -> Controller: ์š”์ฒญ ๊ฒ€์ฆ\n(ํ•„์ˆ˜ ํ•„๋“œ, URL ์œ ํšจ์„ฑ) + +Controller -> Service: updateEventContent(eventDraftId, userId,\nselectedImageUrl, editedContent) +activate Service + +== 1๋‹จ๊ณ„: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์กฐํšŒ ๋ฐ ๊ฒ€์ฆ == + +Service -> Repo: findById(eventDraftId) +activate Repo +Repo -> DB: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์กฐํšŒ\n(์ดˆ์•ˆID๋กœ ์กฐํšŒ) +activate DB +DB --> Repo: EventDraft ์—”ํ‹ฐํ‹ฐ\n{๋ชฉ์ , ์ถ”์ฒœ์•ˆ, ์ƒํƒœ} +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +Service -> Service: validateOwnership(userId, eventDraft) +note right +์†Œ์œ ๊ถŒ ๊ฒ€์ฆ: +- ์‚ฌ์šฉ์žID์™€ ์ดˆ์•ˆ ์†Œ์œ ์ž ์ผ์น˜ ํ™•์ธ +- ๊ถŒํ•œ ์—†์œผ๋ฉด 403 Forbidden +end note + +Service -> Service: validateImageUrl(selectedImageUrl) +note right +์ด๋ฏธ์ง€ URL ๊ฒ€์ฆ: +- URL ํ˜•์‹ ์œ ํšจ์„ฑ +- CDN ๊ฒฝ๋กœ ํ™•์ธ +- ์ด๋ฏธ์ง€ ์กด์žฌ ์—ฌ๋ถ€ +end note + +== 2๋‹จ๊ณ„: ์ฝ˜ํ…์ธ  ํŽธ์ง‘ ์ ์šฉ == + +Service -> Service: applyContentEdits(eventDraft, editedContent) +note right +ํŽธ์ง‘ ๋‚ด์šฉ ์ ์šฉ: +- ์ œ๋ชฉ ํ…์ŠคํŠธ +- ๊ฒฝํ’ˆ ์ •๋ณด ํ…์ŠคํŠธ +- ์ฐธ์—ฌ ์•ˆ๋‚ด ํ…์ŠคํŠธ +- ๋ฐฐ๊ฒฝ์ƒ‰ +- ํ…์ŠคํŠธ ์ƒ‰์ƒ +- ๊ฐ•์กฐ ์ƒ‰์ƒ +end note + +== 3๋‹จ๊ณ„: DB ์ €์žฅ == + +Service -> Repo: update(eventDraft) +activate Repo +Repo -> DB: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์—…๋ฐ์ดํŠธ\n(์„ ํƒ๋œ ์ด๋ฏธ์ง€ URL,\nํŽธ์ง‘๋œ ์ œ๋ชฉ/ํ…์ŠคํŠธ,\n๋ฐฐ๊ฒฝ์ƒ‰/ํ…์ŠคํŠธ์ƒ‰,\n์ˆ˜์ •์ผ์‹œ ์ €์žฅ) +activate DB +DB --> Repo: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ +deactivate DB +Repo --> Service: EventDraft entity +deactivate Repo + +== 4๋‹จ๊ณ„: ์บ์‹œ ๋ฌดํšจํ™” == + +Service -> Cache: ์บ์‹œ ์‚ญ์ œ\nKey: content:image:{eventDraftId} +activate Cache +note right +Redis ์บ์‹œ ๋ฌดํšจํ™”: +- ์ด๋ฏธ์ง€ URL ์บ์‹œ ์‚ญ์ œ +- ์„ ํƒ ์™„๋ฃŒ ํ›„ ๋ถˆํ•„์š” +end note +Cache --> Service: ์‚ญ์ œ ์™„๋ฃŒ +deactivate Cache + +== 5๋‹จ๊ณ„: ์‘๋‹ต ๋ฐ˜ํ™˜ == + +Service --> Controller: EventContentResponse\n{eventDraftId, selectedImageUrl,\neditedContent} +deactivate Service + +Controller --> Gateway: 200 OK\n{"eventDraftId": 123,\n"status": "์ฝ˜ํ…์ธ  ์„ ํƒ ์™„๋ฃŒ",\n"selectedImageUrl": "...",\n"editedContent": {...}} +deactivate Controller + +Gateway --> Client: 200 OK\n์ฝ˜ํ…์ธ  ์„ ํƒ ์™„๋ฃŒ +deactivate Gateway + +note over Client, Cache +**์ €์žฅ ๋‚ด์šฉ** +- ์„ ํƒ๋œ ์ด๋ฏธ์ง€ URL +- ํŽธ์ง‘๋œ ํ…์ŠคํŠธ (์ œ๋ชฉ, ๊ฒฝํ’ˆ ์ •๋ณด, ์ฐธ์—ฌ ์•ˆ๋‚ด) +- ํŽธ์ง‘๋œ ์ƒ‰์ƒ (๋ฐฐ๊ฒฝ์ƒ‰, ํ…์ŠคํŠธ์ƒ‰, ๊ฐ•์กฐ์ƒ‰) +- ๋‹ค์Œ ๋‹จ๊ณ„: ์ตœ์ข… ์Šน์ธ ๋ฐ ๋ฐฐํฌ + +**์„ฑ๋Šฅ ๋ชฉํ‘œ** +- ์‘๋‹ต ์‹œ๊ฐ„: 0.5์ดˆ ์ด๋‚ด +- DB ์—…๋ฐ์ดํŠธ: 0.1์ดˆ +- ์บ์‹œ ์‚ญ์ œ: 0.01์ดˆ +end note + +@enduml diff --git a/design/backend/sequence/inner/participation-๋‹น์ฒจ์ž์ถ”์ฒจ.puml b/design/backend/sequence/inner/participation-๋‹น์ฒจ์ž์ถ”์ฒจ.puml new file mode 100644 index 0000000..63b9381 --- /dev/null +++ b/design/backend/sequence/inner/participation-๋‹น์ฒจ์ž์ถ”์ฒจ.puml @@ -0,0 +1,164 @@ +@startuml participation-๋‹น์ฒจ์ž์ถ”์ฒจ +!theme mono + +title Participation Service - ๋‹น์ฒจ์ž ์ถ”์ฒจ ๋‚ด๋ถ€ ์‹œํ€€์Šค + +actor "์‚ฌ์žฅ๋‹˜" as Owner +participant "API Gateway" as Gateway +participant "ParticipationController" as Controller +participant "ParticipationService" as Service +participant "LotteryAlgorithm" as Lottery +participant "ParticipantRepository" as Repo +participant "DrawLogRepository" as LogRepo +database "Participation DB" as DB + +== UFR-PART-030: ๋‹น์ฒจ์ž ์ถ”์ฒจ == + +Owner -> Gateway: POST /api/v1/events/{eventId}/draw-winners\n{winnerCount, visitBonus, algorithm} +activate Gateway + +Gateway -> Gateway: JWT ํ† ํฐ ๊ฒ€์ฆ\n- ํ† ํฐ ์œ ํšจ์„ฑ ํ™•์ธ\n- ์‚ฌ์žฅ๋‹˜ ๊ถŒํ•œ ํ™•์ธ + +alt JWT ๊ฒ€์ฆ ์‹คํŒจ + Gateway --> Owner: 401 Unauthorized + deactivate Gateway +else JWT ๊ฒ€์ฆ ์„ฑ๊ณต + + Gateway -> Controller: POST /participations/draw-winners\n{eventId, winnerCount, visitBonus} + activate Controller + + Controller -> Controller: ์š”์ฒญ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ\n- eventId ํ•„์ˆ˜\n- winnerCount > 0\n- winnerCount <= ์ฐธ์—ฌ์ž ์ˆ˜ + + alt ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ + Controller --> Gateway: 400 Bad Request + Gateway --> Owner: 400 Bad Request + deactivate Controller + deactivate Gateway + else ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์„ฑ๊ณต + + Controller -> Service: drawWinners(eventId, winnerCount, visitBonus) + activate Service + + Service -> Service: ์ด๋ฒคํŠธ ์ƒํƒœ ํ™•์ธ\n- ์ด๋ฒคํŠธ ์ข…๋ฃŒ ์—ฌ๋ถ€\n- ์ด๋ฏธ ์ถ”์ฒจ ์™„๋ฃŒ ์—ฌ๋ถ€ + + Service -> LogRepo: findByEventId(eventId) + activate LogRepo + LogRepo -> DB: ์ถ”์ฒจ ๋กœ๊ทธ ์กฐํšŒ\n(์ด๋ฒคํŠธID๋กœ ์กฐํšŒ) + activate DB + DB --> LogRepo: ์ถ”์ฒจ ๋กœ๊ทธ ์กฐํšŒ + deactivate DB + LogRepo --> Service: Optional + deactivate LogRepo + + alt ์ด๋ฏธ ์ถ”์ฒจ ์™„๋ฃŒ + Service --> Controller: AlreadyDrawnException + Controller --> Gateway: 409 Conflict\n{message: "์ด๋ฏธ ์ถ”์ฒจ์ด ์™„๋ฃŒ๋œ ์ด๋ฒคํŠธ์ž…๋‹ˆ๋‹ค"} + Gateway --> Owner: 409 Conflict + deactivate Service + deactivate Controller + deactivate Gateway + else ์ถ”์ฒจ ๊ฐ€๋Šฅ ์ƒํƒœ + + Service -> Repo: findAllByEventIdAndIsWinner(eventId, false) + activate Repo + Repo -> DB: ๋ฏธ๋‹น์ฒจ ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ\n(์ด๋ฒคํŠธID๋กœ ๋‹น์ฒจ๋˜์ง€ ์•Š์€\n์ฐธ์—ฌ์ž ์ „์ฒด ์กฐํšŒ,\n์ฐธ์—ฌ์ผ์‹œ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ) + activate DB + DB --> Repo: ์ „์ฒด ์ฐธ์—ฌ์ž ๋ชฉ๋ก + deactivate DB + Repo --> Service: List + deactivate Repo + + alt ์ฐธ์—ฌ์ž ์ˆ˜ ๋ถ€์กฑ + Service --> Controller: InsufficientParticipantsException + Controller --> Gateway: 400 Bad Request\n{message: "์ฐธ์—ฌ์ž ์ˆ˜๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"} + Gateway --> Owner: 400 Bad Request + deactivate Service + deactivate Controller + deactivate Gateway + else ์ถ”์ฒจ ์ง„ํ–‰ + + Service -> Lottery: executeLottery(participants, winnerCount, visitBonus) + activate Lottery + + note right of Lottery + ์ถ”์ฒจ ์•Œ๊ณ ๋ฆฌ์ฆ˜: + ์‹œ๊ฐ„ ๋ณต์žก๋„: O(n log n) + ๊ณต๊ฐ„ ๋ณต์žก๋„: O(n) + + 1. ๋‚œ์ˆ˜ ์ƒ์„ฑ (Crypto.randomBytes) + 2. ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ฐ€์‚ฐ์  ์ ์šฉ (์˜ต์…˜) + - ๋ฐฉ๋ฌธ ๊ณ ๊ฐ: ๊ฐ€์ค‘์น˜ 2๋ฐฐ + - ๋น„๋ฐฉ๋ฌธ ๊ณ ๊ฐ: ๊ฐ€์ค‘์น˜ 1๋ฐฐ + 3. Fisher-Yates Shuffle + - ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ํ™•๋ฅ  ๋ถ„ํฌ + - ๋ฌด์ž‘์œ„ ์„ž๊ธฐ + 4. ์ƒ์œ„ N๋ช… ์„ ์ • + end note + + Lottery -> Lottery: Step 1: ๋‚œ์ˆ˜ ์‹œ๋“œ ์ƒ์„ฑ\n- Crypto.randomBytes(32)\n- ์˜ˆ์ธก ๋ถˆ๊ฐ€๋Šฅํ•œ ๋‚œ์ˆ˜ ๋ณด์žฅ + + Lottery -> Lottery: Step 2: ๊ฐ€์‚ฐ์  ์ ์šฉ\n- visitBonus = true์ผ ๊ฒฝ์šฐ\n- ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ฒฝ๋กœ ์ฐธ์—ฌ์ž ๊ฐ€์ค‘์น˜ ์ฆ๊ฐ€ + + Lottery -> Lottery: Step 3: Fisher-Yates Shuffle\n- ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ํ™•๋ฅ  ๋ถ„ํฌ\n- O(n) ์‹œ๊ฐ„ ๋ณต์žก๋„ + + Lottery -> Lottery: Step 4: ๋‹น์ฒจ์ž ์„ ์ •\n- ์ƒ์œ„ winnerCount๋ช… ์ถ”์ถœ + + Lottery --> Service: List ๋‹น์ฒจ์ž ๋ชฉ๋ก + deactivate Lottery + + Service -> Service: DB ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ + + alt DB ์ €์žฅ ์‹คํŒจ ์‹œ + note right of Service + ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ์ฒ˜๋ฆฌ: + - ๋‹น์ฒจ์ž ์—…๋ฐ์ดํŠธ ์ทจ์†Œ + - ์ถ”์ฒจ ๋กœ๊ทธ ์ €์žฅ ์ทจ์†Œ + - ์žฌ์‹œ๋„ ๊ฐ€๋Šฅ ์ƒํƒœ ์œ ์ง€ + end note + end + + Service -> Repo: updateWinners(winnerIds) + activate Repo + Repo -> DB: ๋‹น์ฒจ์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ\n(๋‹น์ฒจ ์—ฌ๋ถ€๋ฅผ true๋กœ,\n๋‹น์ฒจ ์ผ์‹œ๋ฅผ ํ˜„์žฌ ์‹œ๊ฐ์œผ๋กœ ์„ค์ •,\n๋Œ€์ƒ: ์„ ์ •๋œ ์ฐธ์—ฌ์žID ๋ชฉ๋ก) + activate DB + DB --> Repo: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ + deactivate DB + Repo --> Service: ์—…๋ฐ์ดํŠธ ๊ฑด์ˆ˜ + deactivate Repo + + Service -> Service: DrawLog ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ\n- drawLogId (UUID)\n- eventId\n- drawMethod: "RANDOM"\n- algorithm: "FISHER_YATES_SHUFFLE"\n- visitBonusApplied\n- winnerCount\n- drawnAt (ํ˜„์žฌ์‹œ๊ฐ) + + Service -> LogRepo: save(drawLog) + activate LogRepo + LogRepo -> DB: ์ถ”์ฒจ ๋กœ๊ทธ ์ €์žฅ\n(์ถ”์ฒจ๋กœ๊ทธID, ์ด๋ฒคํŠธID,\n์ถ”์ฒจ๋ฐฉ๋ฒ•, ์•Œ๊ณ ๋ฆฌ์ฆ˜,\n๊ฐ€์‚ฐ์ ์ ์šฉ์—ฌ๋ถ€, ๋‹น์ฒจ์ธ์›,\n์ถ”์ฒจ์ผ์‹œ ์ €์žฅ) + activate DB + note right of DB + ์ถ”์ฒจ ๋กœ๊ทธ ์ €์žฅ: + - ์ถ”์ฒจ ์ผ์‹œ ๊ธฐ๋ก + - ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋ฒ„์ „ ๊ธฐ๋ก + - ๊ฐ€์‚ฐ์  ์ ์šฉ ์—ฌ๋ถ€ + - ๊ฐ์‚ฌ ์ถ”์  ๋ชฉ์  + end note + DB --> LogRepo: ๋กœ๊ทธ ์ €์žฅ ์™„๋ฃŒ + deactivate DB + LogRepo --> Service: DrawLog ์—”ํ‹ฐํ‹ฐ + deactivate LogRepo + + Service -> Service: DB ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ + + Service --> Controller: DrawWinnersResponse\n{๋‹น์ฒจ์ž๋ชฉ๋ก, ์ถ”์ฒจ๋กœ๊ทธID} + deactivate Service + + Controller --> Gateway: 200 OK\n{winners[], drawLogId, message} + deactivate Controller + + Gateway --> Owner: 200 OK + deactivate Gateway + + Owner -> Owner: ๋‹น์ฒจ์ž ๋ชฉ๋ก ํ™”๋ฉด ํ‘œ์‹œ\n- ๋‹น์ฒจ์ž ์ •๋ณด ํ…Œ์ด๋ธ”\n- ์žฌ์ถ”์ฒจ ๋ฒ„ํŠผ\n- ์ถ”์ฒจ ์™„๋ฃŒ ๋ฉ”์‹œ์ง€ + end + end + end +end + +@enduml diff --git a/design/backend/sequence/inner/participation-์ด๋ฒคํŠธ์ฐธ์—ฌ.puml b/design/backend/sequence/inner/participation-์ด๋ฒคํŠธ์ฐธ์—ฌ.puml new file mode 100644 index 0000000..0f144b7 --- /dev/null +++ b/design/backend/sequence/inner/participation-์ด๋ฒคํŠธ์ฐธ์—ฌ.puml @@ -0,0 +1,129 @@ +@startuml participation-์ด๋ฒคํŠธ์ฐธ์—ฌ +!theme mono + +title Participation Service - ์ด๋ฒคํŠธ ์ฐธ์—ฌ ๋‚ด๋ถ€ ์‹œํ€€์Šค + +actor "๊ณ ๊ฐ" as Customer +participant "API Gateway" as Gateway +participant "ParticipationController" as Controller +participant "ParticipationService" as Service +participant "ParticipantRepository" as Repo +database "Participation DB" as DB +participant "KafkaProducer" as Kafka +database "Redis Cache<>" as Cache + +== UFR-PART-010: ์ด๋ฒคํŠธ ์ฐธ์—ฌ == + +Customer -> Gateway: POST /api/v1/participations\n{name, phone, eventId, entryPath, consent} +activate Gateway + +note right of Gateway + ๋น„ํšŒ์› ์ฐธ์—ฌ ๊ฐ€๋Šฅ + JWT ๊ฒ€์ฆ ๋ถˆํ•„์š” +end note + +Gateway -> Controller: POST /participations/register\n{name, phone, eventId, entryPath, consent} +activate Controller + +Controller -> Controller: ์š”์ฒญ ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ\n- ์ด๋ฆ„ 2์ž ์ด์ƒ\n- ์ „ํ™”๋ฒˆํ˜ธ ํ˜•์‹ (์ •๊ทœ์‹)\n- ๊ฐœ์ธ์ •๋ณด ๋™์˜ ํ•„์ˆ˜ + +alt ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ + Controller --> Gateway: 400 Bad Request\n{message: "์œ ํšจ์„ฑ ์˜ค๋ฅ˜"} + Gateway --> Customer: 400 Bad Request + deactivate Controller + deactivate Gateway +else ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์„ฑ๊ณต + + Controller -> Service: registerParticipant(request) + activate Service + + Service -> Cache: GET duplicate_check:{eventId}:{phone} + activate Cache + Cache --> Service: ์บ์‹œ ํ™•์ธ ๊ฒฐ๊ณผ + deactivate Cache + + alt ์บ์‹œ HIT: ์ค‘๋ณต ์ฐธ์—ฌ + Service --> Controller: DuplicateParticipationException + Controller --> Gateway: 409 Conflict\n{message: "์ด๋ฏธ ์ฐธ์—ฌํ•˜์‹  ์ด๋ฒคํŠธ์ž…๋‹ˆ๋‹ค"} + Gateway --> Customer: 409 Conflict + deactivate Service + deactivate Controller + deactivate Gateway + else ์บ์‹œ MISS: DB ์กฐํšŒ + + Service -> Repo: findByEventIdAndPhoneNumber(eventId, phone) + activate Repo + Repo -> DB: ์ฐธ์—ฌ์ž ์ค‘๋ณต ํ™•์ธ\n(์ด๋ฒคํŠธID, ์ „ํ™”๋ฒˆํ˜ธ๋กœ ์กฐํšŒ) + activate DB + DB --> Repo: ์กฐํšŒ ๊ฒฐ๊ณผ + deactivate DB + Repo --> Service: Optional + deactivate Repo + + alt DB์— ์ค‘๋ณต ์ฐธ์—ฌ ์กด์žฌ + Service -> Cache: SET duplicate_check:{eventId}:{phone} = true\nTTL: 7์ผ + activate Cache + Cache --> Service: ์บ์‹œ ์ €์žฅ ์™„๋ฃŒ + deactivate Cache + + Service --> Controller: DuplicateParticipationException + Controller --> Gateway: 409 Conflict\n{message: "์ด๋ฏธ ์ฐธ์—ฌํ•˜์‹  ์ด๋ฒคํŠธ์ž…๋‹ˆ๋‹ค"} + Gateway --> Customer: 409 Conflict + deactivate Service + deactivate Controller + deactivate Gateway + else ์‹ ๊ทœ ์ฐธ์—ฌ: ์ €์žฅ ์ง„ํ–‰ + + Service -> Service: ์‘๋ชจ ๋ฒˆํ˜ธ ์ƒ์„ฑ\n- UUID ๊ธฐ๋ฐ˜\n- ํ˜•์‹: EVT-{timestamp}-{random} + + Service -> Service: Participant ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ\n- participantId (UUID)\n- eventId\n- name, phoneNumber\n- entryPath\n- applicationNumber (์‘๋ชจ๋ฒˆํ˜ธ)\n- participatedAt (ํ˜„์žฌ์‹œ๊ฐ) + + Service -> Repo: save(participant) + activate Repo + Repo -> DB: ์ฐธ์—ฌ์ž ์ •๋ณด ์ €์žฅ\n(์ฐธ์—ฌ์žID, ์ด๋ฒคํŠธID, ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ,\n์ฐธ์—ฌ๊ฒฝ๋กœ, ์‘๋ชจ๋ฒˆํ˜ธ, ์ฐธ์—ฌ์ผ์‹œ,\n๋งˆ์ผ€ํŒ…๋™์˜์—ฌ๋ถ€) + activate DB + DB --> Repo: ์ €์žฅ ์™„๋ฃŒ + deactivate DB + Repo --> Service: Participant ์—”ํ‹ฐํ‹ฐ ๋ฐ˜ํ™˜ + deactivate Repo + + note right of Service + ์ฐธ์—ฌ์ž ๋“ฑ๋ก ์™„๋ฃŒ ํ›„ + ์บ์‹ฑ ๋ฐ ์ด๋ฒคํŠธ ๋ฐœํ–‰ + end note + + Service -> Cache: SET duplicate_check:{eventId}:{phone} = true\nTTL: 7์ผ + activate Cache + Cache --> Service: ์บ์‹œ ์ €์žฅ ์™„๋ฃŒ + deactivate Cache + + Service -> Kafka: Publish Event\n"ParticipantRegistered"\nTopic: participant-events + activate Kafka + note right of Kafka + Event Payload: + { + "participantId": "UUID", + "eventId": "UUID", + "phoneNumber": "010-1234-5678", + "entryPath": "SNS", + "registeredAt": "2025-10-22T10:30:00Z" + } + end note + Kafka --> Service: ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์™„๋ฃŒ + deactivate Kafka + + Service -> Service: ๋‹น์ฒจ ๋ฐœํ‘œ์ผ ๊ณ„์‚ฐ\n- ์ด๋ฒคํŠธ ์ข…๋ฃŒ์ผ + 3์ผ + + Service --> Controller: ParticipationResponse\n{์‘๋ชจ๋ฒˆํ˜ธ, ๋‹น์ฒจ๋ฐœํ‘œ์ผ, ์ฐธ์—ฌ์™„๋ฃŒ๋ฉ”์‹œ์ง€} + deactivate Service + + Controller --> Gateway: 201 Created\n{applicationNumber, drawDate, message} + deactivate Controller + + Gateway --> Customer: 201 Created\n์ฐธ์—ฌ ์™„๋ฃŒ ํ™”๋ฉด ํ‘œ์‹œ + deactivate Gateway + end + end +end + +@enduml diff --git a/design/backend/sequence/inner/participation-์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ.puml b/design/backend/sequence/inner/participation-์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ.puml new file mode 100644 index 0000000..82e6d75 --- /dev/null +++ b/design/backend/sequence/inner/participation-์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ.puml @@ -0,0 +1,120 @@ +@startuml participation-์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ +!theme mono + +title Participation Service - ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ ๋‚ด๋ถ€ ์‹œํ€€์Šค + +actor "์‚ฌ์žฅ๋‹˜" as Owner +participant "API Gateway" as Gateway +participant "ParticipationController" as Controller +participant "ParticipationService" as Service +participant "ParticipantRepository" as Repo +database "Participation DB" as DB +database "Redis Cache<>" as Cache + +== UFR-PART-020: ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ == + +Owner -> Gateway: GET /api/v1/events/{eventId}/participants\n?entryPath={๊ฒฝ๋กœ}&isWinner={๋‹น์ฒจ์—ฌ๋ถ€}\n&name={์ด๋ฆ„}&phone={์ „ํ™”๋ฒˆํ˜ธ}\n&page={ํŽ˜์ด์ง€}&size={ํฌ๊ธฐ} +activate Gateway + +Gateway -> Gateway: JWT ํ† ํฐ ๊ฒ€์ฆ\n- ํ† ํฐ ์œ ํšจ์„ฑ ํ™•์ธ\n- ์‚ฌ์žฅ๋‹˜ ๊ถŒํ•œ ํ™•์ธ + +alt JWT ๊ฒ€์ฆ ์‹คํŒจ + Gateway --> Owner: 401 Unauthorized + deactivate Gateway +else JWT ๊ฒ€์ฆ ์„ฑ๊ณต + + Gateway -> Controller: GET /participants\n{eventId, filters, pagination} + activate Controller + + Controller -> Controller: ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ\n- eventId ํ•„์ˆ˜\n- page >= 0\n- size: 10~100 + + alt ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ + Controller --> Gateway: 400 Bad Request + Gateway --> Owner: 400 Bad Request + deactivate Controller + deactivate Gateway + else ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์„ฑ๊ณต + + Controller -> Service: getParticipantList(eventId, filters, pageable) + activate Service + + Service -> Service: ์บ์‹œ ํ‚ค ์ƒ์„ฑ\n- participant_list:{eventId}:{filters}:{page} + + Service -> Cache: GET participant_list:{key} + activate Cache + Cache --> Service: ์บ์‹œ ์กฐํšŒ ๊ฒฐ๊ณผ + deactivate Cache + + alt ์บ์‹œ HIT + Service --> Controller: ParticipantListResponse\n(์บ์‹œ๋œ ๋ฐ์ดํ„ฐ) + note right of Service + ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + - TTL: 10๋ถ„ + - ์‹ค์‹œ๊ฐ„ ์ •ํ™•๋„ vs ์„ฑ๋Šฅ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ + end note + Controller --> Gateway: 200 OK\n{participants, totalElements, totalPages} + Gateway --> Owner: 200 OK\n์ฐธ์—ฌ์ž ๋ชฉ๋ก ํ‘œ์‹œ + deactivate Service + deactivate Controller + deactivate Gateway + else ์บ์‹œ MISS: DB ์กฐํšŒ + + Service -> Service: ๋™์  ์ฟผ๋ฆฌ ์ƒ์„ฑ\n- ์ฐธ์—ฌ ๊ฒฝ๋กœ ํ•„ํ„ฐ\n- ๋‹น์ฒจ ์—ฌ๋ถ€ ํ•„ํ„ฐ\n- ์ด๋ฆ„/์ „ํ™”๋ฒˆํ˜ธ ๊ฒ€์ƒ‰ + + Service -> Repo: findParticipants(eventId, filters, pageable) + activate Repo + + Repo -> DB: ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ\n(์ด๋ฒคํŠธID, ์ฐธ์—ฌ๊ฒฝ๋กœ, ๋‹น์ฒจ์—ฌ๋ถ€,\n์ด๋ฆ„/์ „ํ™”๋ฒˆํ˜ธ ๊ฒ€์ƒ‰์กฐ๊ฑด์œผ๋กœ ํ•„ํ„ฐ๋งํ•˜์—ฌ\n์ฐธ์—ฌ์ผ์‹œ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ํŽ˜์ด์ง• ์กฐํšŒ) + activate DB + + note right of DB + ๋™์  ์ฟผ๋ฆฌ ์กฐ๊ฑด: + - entryPath ํ•„ํ„ฐ (์„ ํƒ) + - isWinner ํ•„ํ„ฐ (์„ ํƒ) + - name/phone ๊ฒ€์ƒ‰ (์„ ํƒ) + - ํŽ˜์ด์ง€๋„ค์ด์…˜ (ํ•„์ˆ˜) + + ํ•„์š” ์ธ๋ฑ์Šค: + idx_participants_event_filters + (event_id, entry_path, is_winner, participated_at DESC) + end note + + DB --> Repo: ์ฐธ์—ฌ์ž ๋ชฉ๋ก ๊ฒฐ๊ณผ์…‹ + deactivate DB + + Repo -> DB: ์ „์ฒด ์ฐธ์—ฌ์ž ์ˆ˜ ์กฐํšŒ\n(๋™์ผํ•œ ํ•„ํ„ฐ ์กฐ๊ฑด ์ ์šฉ) + activate DB + DB --> Repo: ์ „์ฒด ๊ฑด์ˆ˜ + deactivate DB + + Repo --> Service: Page + deactivate Repo + + Service -> Service: DTO ๋ณ€ํ™˜\n- ์ „ํ™”๋ฒˆํ˜ธ ๋งˆ์Šคํ‚น (010-****-1234)\n- ์‘๋ชจ๋ฒˆํ˜ธ ํ˜•์‹ํ™”\n- ๋‹น์ฒจ ์—ฌ๋ถ€ ๋ผ๋ฒจ ๋ณ€ํ™˜ + + Service -> Cache: SET participant_list:{key} = data\nTTL: 10๋ถ„ + activate Cache + note right of Cache + ์บ์‹œ ์ €์žฅ: + - TTL: 10๋ถ„ + - ์‹ค์‹œ๊ฐ„ ์ฐธ์—ฌ ๋ฐ˜์˜๊ณผ ์„ฑ๋Šฅ ๊ท ํ˜• + - ์ด๋ฒคํŠธ ์ฐธ์—ฌ ๋นˆ๋„ ๊ณ ๋ ค + end note + Cache --> Service: ์บ์‹œ ์ €์žฅ ์™„๋ฃŒ + deactivate Cache + + Service --> Controller: ParticipantListResponse\n{participants[], totalElements, totalPages, currentPage} + deactivate Service + + Controller --> Gateway: 200 OK\n{data, pagination} + deactivate Controller + + Gateway --> Owner: 200 OK + deactivate Gateway + + Owner -> Owner: ์ฐธ์—ฌ์ž ๋ชฉ๋ก ํ™”๋ฉด ํ‘œ์‹œ\n- ํ…Œ์ด๋ธ” ํ˜•ํƒœ\n- ํŽ˜์ด์ง€๋„ค์ด์…˜\n- ํ•„ํ„ฐ/๊ฒ€์ƒ‰ UI + end + end +end + +@enduml diff --git a/design/backend/sequence/inner/user-๋กœ๊ทธ์•„์›ƒ.puml b/design/backend/sequence/inner/user-๋กœ๊ทธ์•„์›ƒ.puml new file mode 100644 index 0000000..88f1d24 --- /dev/null +++ b/design/backend/sequence/inner/user-๋กœ๊ทธ์•„์›ƒ.puml @@ -0,0 +1,155 @@ +@startuml user-๋กœ๊ทธ์•„์›ƒ +!theme mono + +title User Service - ๋กœ๊ทธ์•„์›ƒ ๋‚ด๋ถ€ ์‹œํ€€์Šค (UFR-USER-040) + +actor Client +participant "UserController" as Controller <> +participant "AuthenticationService" as AuthService <> +participant "JwtTokenProvider" as JwtProvider <> +participant "Redis\nCache" as Redis <> + +note over Controller, Redis +**UFR-USER-040: ๋กœ๊ทธ์•„์›ƒ** +- JWT ํ† ํฐ ๊ฒ€์ฆ +- Redis ์„ธ์…˜ ์‚ญ์ œ +- ํด๋ผ์ด์–ธํŠธ ์ธก ํ† ํฐ ์‚ญ์ œ (ํ”„๋ก ํŠธ์—”๋“œ ์ฒ˜๋ฆฌ) +end note + +Client -> Controller: POST /api/users/logout\nAuthorization: Bearer {JWT} +activate Controller + +Controller -> Controller: @AuthenticationPrincipal\n(JWT์—์„œ userId ์ถ”์ถœ) + +Controller -> Controller: JWT ํ† ํฐ ์ถ”์ถœ\n(Authorization ํ—ค๋”์—์„œ) + +Controller -> AuthService: logout(token, userId) +activate AuthService + +== 1๋‹จ๊ณ„: JWT ํ† ํฐ ๊ฒ€์ฆ == + +AuthService -> JwtProvider: validateToken(token) +activate JwtProvider +JwtProvider -> JwtProvider: JWT ์„œ๋ช… ๊ฒ€์ฆ\n(๋งŒ๋ฃŒ ์‹œ๊ฐ„ ํ™•์ธ) +JwtProvider --> AuthService: boolean (์œ ํšจ ์—ฌ๋ถ€) +deactivate JwtProvider + +alt JWT ํ† ํฐ ๋ฌดํšจ + AuthService --> Controller: throw InvalidTokenException\n("์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค") + Controller --> Client: 401 Unauthorized\n{"code": "AUTH_002",\n"error": "์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค"} + deactivate AuthService + deactivate Controller + +else JWT ํ† ํฐ ์œ ํšจ + + == 2๋‹จ๊ณ„: Redis ์„ธ์…˜ ์‚ญ์ œ == + + AuthService -> Redis: ์„ธ์…˜ ์‚ญ์ œ\n(์บ์‹œํ‚ค: user:session:{token}) + activate Redis + Redis --> AuthService: ์‚ญ์ œ๋œ ํ‚ค ๊ฐœ์ˆ˜ (0 ๋˜๋Š” 1) + deactivate Redis + + alt ์„ธ์…˜ ์—†์Œ (์ด๋ฏธ ๋กœ๊ทธ์•„์›ƒ๋จ) + note right of AuthService + **๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ** + - ์„ธ์…˜์ด ์—†์–ด๋„ ๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ + - ์ค‘๋ณต ๋กœ๊ทธ์•„์›ƒ ์š”์ฒญ์— ์•ˆ์ „ + end note + else ์„ธ์…˜ ์žˆ์Œ (์ •์ƒ ๋กœ๊ทธ์•„์›ƒ) + note right of AuthService + **์„ธ์…˜ ์‚ญ์ œ ์™„๋ฃŒ** + - Redis์—์„œ ์„ธ์…˜ ์ •๋ณด ์ œ๊ฑฐ + - JWT ํ† ํฐ ๋ฌดํšจํ™” (Blacklist ๋ฐฉ์‹) + end note + end + + == 3๋‹จ๊ณ„: JWT ํ† ํฐ Blacklist ์ถ”๊ฐ€ (์„ ํƒ์ ) == + + note right of AuthService + **JWT Blacklist ์ „๋žต** + - ๋งŒ๋ฃŒ๋˜์ง€ ์•Š์€ JWT ํ† ํฐ์„ ๊ฐ•์ œ๋กœ ๋ฌดํšจํ™” + - Redis์— ํ† ํฐ์„ Blacklist์— ์ถ”๊ฐ€ (TTL: ๋‚จ์€ ๋งŒ๋ฃŒ ์‹œ๊ฐ„) + - API Gateway์—์„œ Blacklist ํ™•์ธ + + **API Gateway ์—ฐ๊ณ„ ์‹œ๋‚˜๋ฆฌ์˜ค** + 1. ๋กœ๊ทธ์•„์›ƒ: AuthService๊ฐ€ Blacklist์— ํ† ํฐ ์ถ”๊ฐ€ + 2. ํ›„์† API ์š”์ฒญ: API Gateway๊ฐ€ Blacklist ํ™•์ธ + - Redis GET jwt:blacklist:{token} + - ์กด์žฌํ•˜๋ฉด: 401 Unauthorized ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + - ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด: ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค๋กœ ๋ผ์šฐํŒ… + 3. ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ๋„๋‹ฌ: Redis TTL ๋งŒ๋ฃŒ๋กœ ์ž๋™ ์‚ญ์ œ + end note + + AuthService -> JwtProvider: getRemainingExpiration(token) + activate JwtProvider + JwtProvider -> JwtProvider: JWT Claims์—์„œ\nexp(๋งŒ๋ฃŒ ์‹œ๊ฐ„) ์ถ”์ถœ\n(ํ˜„์žฌ ์‹œ๊ฐ„๊ณผ ๋น„๊ต) + JwtProvider --> AuthService: remainingSeconds + deactivate JwtProvider + + alt ๋‚จ์€ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ > 0 + AuthService -> Redis: ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ์— ํ† ํฐ ์ถ”๊ฐ€\n(์บ์‹œํ‚ค: jwt:blacklist:{token},\n๊ฐ’: "revoked", TTL: ๋‚จ์€์ดˆ) + activate Redis + Redis --> AuthService: Blacklist ์ถ”๊ฐ€ ์™„๋ฃŒ + deactivate Redis + end + + == 4๋‹จ๊ณ„: ๋กœ๊ทธ์•„์›ƒ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (์„ ํƒ์ ) == + + note right of AuthService + **๋กœ๊ทธ์•„์›ƒ ๋กœ๊น… ๋ฐ ์ด๋ฒคํŠธ** + - ๊ฐ์‚ฌ ๋กœ๊ทธ ๊ธฐ๋ก: userId, timestamp, IP + - ์ด๋ฒคํŠธ ๋ฐœํ–‰: LOGOUT_SUCCESS + - ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘: ์„ธ์…˜ ์ง€์† ์‹œ๊ฐ„, ํ™œ๋™ ํ†ต๊ณ„ + end note + + AuthService -> AuthService: ๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต ๋กœ๊ทธ ๊ธฐ๋ก\n(userId, timestamp, sessionDuration) + + AuthService ->> AuthService: publishEvent(LOGOUT_SUCCESS) + note right of AuthService + **์ด๋ฒคํŠธ ํ™œ์šฉ** + - ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ + - ๋ถ„์„ ์‹œ์Šคํ…œ ์—ฐ๋™ + - ๊ฐ์‚ฌ ๋กœ๊ทธ ์ €์žฅ์†Œ ์ „์†ก + end note + + == 5๋‹จ๊ณ„: ์‘๋‹ต ๋ฐ˜ํ™˜ == + + AuthService --> Controller: LogoutResponse\n(success: true) + deactivate AuthService + + Controller --> Client: 200 OK\n{"success": true,\n"message": "์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"} + deactivate Controller +end + +note over Controller, Redis +**๋ณด์•ˆ ์ฒ˜๋ฆฌ** +- JWT ํ† ํฐ Blacklist: ๋งŒ๋ฃŒ ์ „ ํ† ํฐ ๊ฐ•์ œ ๋ฌดํšจํ™” +- ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ: ์ค‘๋ณต ๋กœ๊ทธ์•„์›ƒ ์š”์ฒญ์— ์•ˆ์ „ +- ์„ธ์…˜ ์™„์ „ ์‚ญ์ œ: Redis์—์„œ ์„ธ์…˜ ์ •๋ณด ์ œ๊ฑฐ +- ๊ฐ์‚ฌ ๋กœ๊ทธ: userId, timestamp, IP, sessionDuration ๊ธฐ๋ก + +**API Gateway ์—ฐ๊ณ„** +- Blacklist ํ™•์ธ: GET jwt:blacklist:{token} +- ์กด์žฌ ์‹œ: 401 Unauthorized ์ฆ‰์‹œ ๋ฐ˜ํ™˜ +- TTL ์ž๋™ ๊ด€๋ฆฌ: ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ๋„๋‹ฌ ์‹œ ์ž๋™ ์‚ญ์ œ + +**ํด๋ผ์ด์–ธํŠธ ์ธก ์ฒ˜๋ฆฌ** +- ํ”„๋ก ํŠธ์—”๋“œ: LocalStorage ๋˜๋Š” Cookie์—์„œ JWT ํ† ํฐ ์‚ญ์ œ +- ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ +- ๋ชจ๋“  ์ธ์ฆ ํ—ค๋” ์ œ๊ฑฐ + +**์„ฑ๋Šฅ ๋ชฉํ‘œ** +- Redis ์‚ญ์ œ ์—ฐ์‚ฐ: O(1) ์‹œ๊ฐ„ ๋ณต์žก๋„ +- ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„: 0.1์ดˆ ์ด๋‚ด +- P95 ์‘๋‹ต ์‹œ๊ฐ„: 0.2์ดˆ ์ด๋‚ด + +**์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ** +- LOGOUT_SUCCESS ์ด๋ฒคํŠธ ๋ฐœํ–‰ (๋น„๋™๊ธฐ) +- ๊ฐ์‚ฌ ๋กœ๊ทธ ์ €์žฅ์†Œ ์ „์†ก +- ๋ถ„์„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ + +**์—๋Ÿฌ ์ฝ”๋“œ** +- AUTH_002: JWT ํ† ํฐ ๋ฌดํšจ +end note + +@enduml diff --git a/design/backend/sequence/inner/user-๋กœ๊ทธ์ธ.puml b/design/backend/sequence/inner/user-๋กœ๊ทธ์ธ.puml new file mode 100644 index 0000000..5afba33 --- /dev/null +++ b/design/backend/sequence/inner/user-๋กœ๊ทธ์ธ.puml @@ -0,0 +1,147 @@ +@startuml user-๋กœ๊ทธ์ธ +!theme mono + +title User Service - ๋กœ๊ทธ์ธ ๋‚ด๋ถ€ ์‹œํ€€์Šค (UFR-USER-020) + +actor Client +participant "UserController" as Controller <> +participant "UserService" as Service <> +participant "AuthenticationService" as AuthService <> +participant "UserRepository" as UserRepo <> +participant "PasswordEncoder" as PwdEncoder <> +participant "JwtTokenProvider" as JwtProvider <> +participant "Redis\nCache" as Redis <> +participant "User DB\n(PostgreSQL)" as UserDB <> + +note over Controller, UserDB +**UFR-USER-020: ๋กœ๊ทธ์ธ** +- ์ž…๋ ฅ: ์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ +- ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ (bcrypt compare) +- JWT ํ† ํฐ ๋ฐœ๊ธ‰ +- ์„ธ์…˜ ์ €์žฅ (Redis) +- ์ตœ์ข… ๋กœ๊ทธ์ธ ์‹œ๊ฐ ์—…๋ฐ์ดํŠธ +end note + +Client -> Controller: POST /api/users/login\n{"email": "user@example.com",\n"password": "password123"} +activate Controller + +Controller -> Controller: ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ\n(ํ•„์ˆ˜ ํ•„๋“œ, ์ด๋ฉ”์ผ ํ˜•์‹ ํ™•์ธ) + +Controller -> AuthService: authenticate(email, password) +activate AuthService + +== 1๋‹จ๊ณ„: ์‚ฌ์šฉ์ž ์กฐํšŒ == + +AuthService -> Service: findByEmail(email) +activate Service +Service -> UserRepo: findByEmail(email) +activate UserRepo +UserRepo -> UserDB: ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ\n(์‚ฌ์šฉ์žID, ๋น„๋ฐ€๋ฒˆํ˜ธํ•ด์‹œ, ์—ญํ• ,\n์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ ์กฐํšŒ) +activate UserDB +UserDB --> UserRepo: ์‚ฌ์šฉ์ž ์ •๋ณด ๋ฐ˜ํ™˜ ๋˜๋Š” ์—†์Œ +deactivate UserDB +UserRepo --> Service: Optional +deactivate UserRepo +Service --> AuthService: Optional +deactivate Service + +alt ์‚ฌ์šฉ์ž ์—†์Œ + AuthService --> Controller: throw AuthenticationFailedException\n("์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”") + Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"message": "์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ\nํ™•์ธํ•ด์ฃผ์„ธ์š”"} + deactivate AuthService + deactivate Controller + +else ์‚ฌ์šฉ์ž ์กด์žฌ + + == 2๋‹จ๊ณ„: ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ == + + AuthService -> PwdEncoder: matches(rawPassword, passwordHash) + activate PwdEncoder + PwdEncoder -> PwdEncoder: bcrypt compare\n(์ž…๋ ฅ ๋น„๋ฐ€๋ฒˆํ˜ธ vs ์ €์žฅ๋œ ํ•ด์‹œ) + PwdEncoder --> AuthService: boolean (์ผ์น˜ ์—ฌ๋ถ€) + deactivate PwdEncoder + + alt ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ + AuthService --> Controller: throw AuthenticationFailedException\n("์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”") + Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"message": "์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ\nํ™•์ธํ•ด์ฃผ์„ธ์š”"} + deactivate AuthService + deactivate Controller + + else ๋น„๋ฐ€๋ฒˆํ˜ธ ์ผ์น˜ + + == 3๋‹จ๊ณ„: JWT ํ† ํฐ ์ƒ์„ฑ == + + AuthService -> JwtProvider: generateToken(userId, role) + activate JwtProvider + JwtProvider -> JwtProvider: JWT ํ† ํฐ ์ƒ์„ฑ\n(Claims: userId, role=OWNER,\nexp=7์ผ) + JwtProvider --> AuthService: JWT ํ† ํฐ + deactivate JwtProvider + + == 4๋‹จ๊ณ„: ์„ธ์…˜ ์ €์žฅ == + + AuthService -> Redis: ์„ธ์…˜ ์ •๋ณด ์ €์žฅ\nKey: user:session:{token}\nValue: {userId, role}\nTTL: 7์ผ + activate Redis + Redis --> AuthService: ์ €์žฅ ์™„๋ฃŒ + deactivate Redis + + == 5๋‹จ๊ณ„: ์ตœ์ข… ๋กœ๊ทธ์ธ ์‹œ๊ฐ ์—…๋ฐ์ดํŠธ (๋น„๋™๊ธฐ) == + + AuthService ->> Service: updateLastLoginAt(userId) + activate Service + note right of Service + **๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ** + - @Async ์–ด๋…ธํ…Œ์ด์…˜ ์‚ฌ์šฉ + - ๋กœ๊ทธ์ธ ์‘๋‹ต ์ง€์—ฐ ๋ฐฉ์ง€ + - ๋ณ„๋„ ์Šค๋ ˆ๋“œํ’€์—์„œ ์‹คํ–‰ + end note + Service ->> UserRepo: updateLastLoginAt(userId) + activate UserRepo + UserRepo ->> UserDB: ์‚ฌ์šฉ์ž ์ตœ์ข… ๋กœ๊ทธ์ธ ์‹œ๊ฐ ๊ฐฑ์‹ \n(ํ˜„์žฌ ์‹œ๊ฐ์œผ๋กœ ์—…๋ฐ์ดํŠธ) + activate UserDB + UserDB -->> UserRepo: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ + deactivate UserDB + UserRepo -->> Service: void + deactivate UserRepo + Service -->> AuthService: void (๋น„๋™๊ธฐ ์™„๋ฃŒ) + deactivate Service + + note over AuthService, Service + **๋น„๋™๊ธฐ ํ™”์‚ดํ‘œ ์„ค๋ช…** + - `->>`: ๋น„๋™๊ธฐ ํ˜ธ์ถœ (ํ˜ธ์ถœ ํ›„ ์ฆ‰์‹œ ๋ฐ˜ํ™˜) + - `-->>`: ๋น„๋™๊ธฐ ์‘๋‹ต (๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ ์™„๋ฃŒ) + end note + + == 6๋‹จ๊ณ„: ์‘๋‹ต ๋ฐ˜ํ™˜ == + + AuthService -> AuthService: ๋กœ๊ทธ์ธ ์‘๋‹ต DTO ์ƒ์„ฑ + AuthService --> Controller: LoginResponse\n{token, userId, userName, role, email} + deactivate AuthService + + Controller --> Client: 200 OK\n{"token": "eyJhbGc...",\n"userId": 123,\n"userName": "ํ™๊ธธ๋™",\n"role": "OWNER",\n"email": "hong@example.com"} + deactivate Controller + end +end + +note over Controller, UserDB +**๋ณด์•ˆ ์ฒ˜๋ฆฌ** +- ๋น„๋ฐ€๋ฒˆํ˜ธ: bcrypt compare (์›๋ณธ ๋…ธ์ถœ ์•ˆ ๋จ) +- ์—๋Ÿฌ ๋ฉ”์‹œ์ง€: ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ๊ตฌ๋ถ„ ์—†์ด ๋™์ผ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜ (Timing Attack ๋ฐฉ์–ด) +- JWT ํ† ํฐ: 7์ผ ๋งŒ๋ฃŒ, ์„œ๋ฒ„ ์„ธ์…˜๊ณผ ๋™๊ธฐํ™” + +**๋ณด์•ˆ ๊ฐ•ํ™” (ํ–ฅํ›„ ๊ตฌํ˜„)** +- Rate Limiting: IP๋‹น 5๋ถ„์— 5ํšŒ ๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ ์ž„์‹œ ์ฐจ๋‹จ (15๋ถ„) +- Account Lockout: ๋™์ผ ๊ณ„์ • 10ํšŒ ์‹คํŒจ ์‹œ ๊ณ„์ • ์ž ๊ธˆ (๊ด€๋ฆฌ์ž ํ•ด์ œ) +- MFA: 2๋‹จ๊ณ„ ์ธ์ฆ ์ถ”๊ฐ€ (SMS/TOTP) +- Anomaly Detection: ๋น„์ •์ƒ ๋กœ๊ทธ์ธ ํŒจํ„ด ๊ฐ์ง€ (์ง€์—ญ, ๋””๋ฐ”์ด์Šค ๋ณ€๊ฒฝ) + +**์„ฑ๋Šฅ ์ตœ์ ํ™”** +- ์ตœ์ข… ๋กœ๊ทธ์ธ ์‹œ๊ฐ ์—…๋ฐ์ดํŠธ: ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ (@Async) +- ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„: 0.5์ดˆ ์ด๋‚ด +- P95 ์‘๋‹ต ์‹œ๊ฐ„: 1.0์ดˆ ์ด๋‚ด +- Redis ์„ธ์…˜ ์กฐํšŒ: 0.1์ดˆ ์ด๋‚ด + +**์—๋Ÿฌ ์ฝ”๋“œ** +- AUTH_001: ์ธ์ฆ ์‹คํŒจ (์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜) +end note + +@enduml diff --git a/design/backend/sequence/inner/user-ํ”„๋กœํ•„์ˆ˜์ •.puml b/design/backend/sequence/inner/user-ํ”„๋กœํ•„์ˆ˜์ •.puml new file mode 100644 index 0000000..1bbaebf --- /dev/null +++ b/design/backend/sequence/inner/user-ํ”„๋กœํ•„์ˆ˜์ •.puml @@ -0,0 +1,233 @@ +@startuml user-ํ”„๋กœํ•„์ˆ˜์ • +!theme mono + +title User Service - ํ”„๋กœํ•„ ์ˆ˜์ • ๋‚ด๋ถ€ ์‹œํ€€์Šค (UFR-USER-030) + +actor Client +participant "UserController" as Controller <> +participant "UserService" as Service <> +participant "UserRepository" as UserRepo <> +participant "StoreRepository" as StoreRepo <> +participant "PasswordEncoder" as PwdEncoder <> +participant "Redis\nCache" as Redis <> +participant "User DB\n(PostgreSQL)" as UserDB <> + +note over Controller, UserDB +**UFR-USER-030: ํ”„๋กœํ•„ ์ˆ˜์ •** +- ๊ธฐ๋ณธ ์ •๋ณด: ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ +- ๋งค์žฅ ์ •๋ณด: ๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ, ์˜์—…์‹œ๊ฐ„ +- ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ (ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ํ•„์ˆ˜) +- ์ „ํ™”๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์‹œ ์žฌ์ธ์ฆ ํ•„์š” (ํ–ฅํ›„ ๊ตฌํ˜„) +end note + +Client -> Controller: PUT /api/users/profile\nAuthorization: Bearer {JWT}\n(UpdateProfileRequest DTO) +activate Controller + +Controller -> Controller: @AuthenticationPrincipal\n(JWT์—์„œ userId ์ถ”์ถœ) + +Controller -> Controller: @Valid ์–ด๋…ธํ…Œ์ด์…˜ ๊ฒ€์ฆ\n(์ด๋ฉ”์ผ ํ˜•์‹, ํ•„๋“œ ๊ธธ์ด ๋“ฑ) + +Controller -> Service: updateProfile(userId, UpdateProfileRequest) +activate Service + +== 1๋‹จ๊ณ„: ๊ธฐ์กด ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ == + +Service -> UserRepo: findById(userId) +activate UserRepo +UserRepo -> UserDB: ์‚ฌ์šฉ์žID๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ\n(์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ) +activate UserDB +UserDB --> UserRepo: ์‚ฌ์šฉ์ž ์ •๋ณด +deactivate UserDB +UserRepo --> Service: User ์—”ํ‹ฐํ‹ฐ +deactivate UserRepo + +alt ์‚ฌ์šฉ์ž ์—†์Œ + Service --> Controller: throw UserNotFoundException\n("์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + Controller --> Client: 404 Not Found\n{"code": "USER_003",\n"error": "์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"} + deactivate Service + deactivate Controller + +else ์‚ฌ์šฉ์ž ์กด์žฌ + + == 2๋‹จ๊ณ„: ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์š”์ฒญ ์ฒ˜๋ฆฌ == + + alt ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ์š”์ฒญ O + Service -> Service: ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ ํ•„์š” ํ™•์ธ + + Service -> PwdEncoder: matches(currentPassword,\nuser.getPasswordHash()) + activate PwdEncoder + PwdEncoder -> PwdEncoder: bcrypt compare + PwdEncoder --> Service: boolean (์ผ์น˜ ์—ฌ๋ถ€) + deactivate PwdEncoder + + alt ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ + Service --> Controller: throw InvalidPasswordException\n("ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค") + Controller --> Client: 400 Bad Request\n{"code": "USER_004",\n"error": "ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€\n์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"} + deactivate Service + deactivate Controller + + else ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ผ์น˜ + Service -> Service: ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ\n(8์ž ์ด์ƒ, ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž) + + Service -> PwdEncoder: encode(newPassword) + activate PwdEncoder + PwdEncoder -> PwdEncoder: bcrypt ํ•ด์‹ฑ\n(Cost Factor 10) + PwdEncoder --> Service: newPasswordHash + deactivate PwdEncoder + + Service -> Service: user.setPasswordHash(newPasswordHash) + end + end + + == 3๋‹จ๊ณ„: ์—”ํ‹ฐํ‹ฐ ์ˆ˜์ • ์ค€๋น„ (๋ฉ”๋ชจ๋ฆฌ์ƒ ๋ณ€๊ฒฝ) == + + note right of Service + **JPA Dirty Checking** + - ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ ์ „ ์—”ํ‹ฐํ‹ฐ ์ˆ˜์ • (๋ฉ”๋ชจ๋ฆฌ์ƒ) + - ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์‹œ ๋ณ€๊ฒฝ ๊ฐ์ง€ํ•˜์—ฌ UPDATE ์ž๋™ ์‹คํ–‰ + - ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๋งŒ UPDATE ์ฟผ๋ฆฌ์— ํฌํ•จ + end note + + alt ์ด๋ฆ„ ๋ณ€๊ฒฝ + Service -> Service: user.setName(newName) + end + + alt ์ „ํ™”๋ฒˆํ˜ธ ๋ณ€๊ฒฝ + Service -> Service: user.setPhoneNumber(newPhoneNumber) + note right of Service + **ํ–ฅํ›„ ๊ตฌํ˜„: ์žฌ์ธ์ฆ ํ•„์š”** + - SMS ์ธ์ฆ ๋˜๋Š” ์ด๋ฉ”์ผ ์ธ์ฆ + - ์ธ์ฆ ์™„๋ฃŒ ํ›„์—๋งŒ ๋ณ€๊ฒฝ ๋ฐ˜์˜ + end note + end + + alt ์ด๋ฉ”์ผ ๋ณ€๊ฒฝ + Service -> Service: user.setEmail(newEmail) + end + + == 4๋‹จ๊ณ„: ๋งค์žฅ ์ •๋ณด ์ˆ˜์ • ์ค€๋น„ (๋ฉ”๋ชจ๋ฆฌ์ƒ ๋ณ€๊ฒฝ) == + + Service -> StoreRepo: findByUserId(userId) + activate StoreRepo + StoreRepo -> UserDB: ์‚ฌ์šฉ์žID๋กœ ๋งค์žฅ ์กฐํšŒ\n(๋งค์žฅ ์ •๋ณด ์กฐํšŒ) + activate UserDB + UserDB --> StoreRepo: ๋งค์žฅ ์ •๋ณด + deactivate UserDB + StoreRepo --> Service: Store ์—”ํ‹ฐํ‹ฐ + deactivate StoreRepo + + alt ๋งค์žฅ๋ช… ๋ณ€๊ฒฝ + Service -> Service: store.setStoreName(newStoreName) + end + + alt ์—…์ข… ๋ณ€๊ฒฝ + Service -> Service: store.setIndustry(newIndustry) + end + + alt ์ฃผ์†Œ ๋ณ€๊ฒฝ + Service -> Service: store.setAddress(newAddress) + end + + alt ์˜์—…์‹œ๊ฐ„ ๋ณ€๊ฒฝ + Service -> Service: store.setBusinessHours(newBusinessHours) + end + + == 5๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŠธ๋žœ์žญ์…˜ == + + Service -> UserDB: ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ + activate UserDB + + note right of Service + **Optimistic Locking** + - @Version ํ•„๋“œ๋กœ ๋™์‹œ ์ˆ˜์ • ๊ฐ์ง€ + - ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์ด ๋จผ์ € ์ˆ˜์ •ํ•œ ๊ฒฝ์šฐ + - OptimisticLockException ๋ฐœ์ƒ + end note + + Service -> UserRepo: save(user) + activate UserRepo + UserRepo -> UserDB: ์‚ฌ์šฉ์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ\n(์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ,\n๋น„๋ฐ€๋ฒˆํ˜ธํ•ด์‹œ, ์ˆ˜์ •์ผ์‹œ,\n๋ฒ„์ „ ์ฆ๊ฐ€)\nOptimistic Lock ์ ์šฉ + UserDB --> UserRepo: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ (1 row affected) + UserRepo --> Service: User ์—”ํ‹ฐํ‹ฐ + deactivate UserRepo + + alt ๋™์‹œ์„ฑ ์ถฉ๋Œ (version ๋ถˆ์ผ์น˜) + UserRepo --> Service: throw OptimisticLockException + Service --> Controller: throw ConcurrentModificationException\n("๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ์ˆ˜์ • ์ค‘์ž…๋‹ˆ๋‹ค") + Controller --> Client: 409 Conflict\n{"code": "USER_005",\n"error": "๋‹ค๋ฅธ ์„ธ์…˜์—์„œ ํ”„๋กœํ•„์„\n์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.\n์ƒˆ๋กœ๊ณ ์นจ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”"} + Service -> UserDB: ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ + deactivate UserDB + deactivate Service + deactivate Controller + else ์ •์ƒ ์—…๋ฐ์ดํŠธ + + Service -> StoreRepo: save(store) + activate StoreRepo + StoreRepo -> UserDB: ๋งค์žฅ ์ •๋ณด ์—…๋ฐ์ดํŠธ\n(๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ,\n์˜์—…์‹œ๊ฐ„, ์ˆ˜์ •์ผ์‹œ,\n๋ฒ„์ „ ์ฆ๊ฐ€)\nOptimistic Lock ์ ์šฉ + UserDB --> StoreRepo: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ (1 row affected) + StoreRepo --> Service: Store ์—”ํ‹ฐํ‹ฐ + deactivate StoreRepo + + Service -> UserDB: ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ + UserDB --> Service: ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์™„๋ฃŒ + deactivate UserDB + + == 6๋‹จ๊ณ„: ์บ์‹œ ๋ฌดํšจํ™” (์„ ํƒ์ ) == + + note right of Service + **์บ์‹œ ๋ฌดํšจํ™” ์ „๋žต** + - ์„ธ์…˜ ์ •๋ณด๋Š” ๋ณ€๊ฒฝ ์—†์Œ (JWT ์œ ์ง€) + - ํ”„๋กœํ•„ ์บ์‹œ๊ฐ€ ์žˆ๋‹ค๋ฉด ๋ฌดํšจํ™” + end note + + alt ํ”„๋กœํ•„ ์บ์‹œ ์‚ฌ์šฉ ์ค‘ + Service -> Redis: ํ”„๋กœํ•„ ์บ์‹œ ์‚ญ์ œ\n(์บ์‹œํ‚ค: user:profile:{userId}) + activate Redis + Redis --> Service: ์บ์‹œ ์‚ญ์ œ ์™„๋ฃŒ + deactivate Redis + end + + == 7๋‹จ๊ณ„: ์‘๋‹ต ๋ฐ˜ํ™˜ == + + Service -> Service: ์‘๋‹ต DTO ์ƒ์„ฑ\n(UpdateProfileResponse) + Service --> Controller: UpdateProfileResponse\n(userId, userName, email,\nstoreId, storeName) + deactivate Service + + Controller --> Client: 200 OK\n{"userId": 123,\n"userName": "ํ™๊ธธ๋™",\n"email": "hong@example.com",\n"storeId": 456,\n"storeName": "๋ง›์žˆ๋Š”์ง‘"} + deactivate Controller + end +end + +note over Controller, UserDB +**Transaction Rollback ์ฒ˜๋ฆฌ** +- ํŠธ๋žœ์žญ์…˜ ์‹คํŒจ ์‹œ ์ž๋™ Rollback +- User/Store UPDATE ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจ ์‹œ ์ „์ฒด ๋กค๋ฐฑ +- OptimisticLockException ๋ฐœ์ƒ ์‹œ 409 Conflict ๋ฐ˜ํ™˜ + +**๋™์‹œ์„ฑ ์ œ์–ด** +- Optimistic Locking: @Version ํ•„๋“œ๋กœ ๋™์‹œ ์ˆ˜์ • ๊ฐ์ง€ +- ์ถฉ๋Œ ๊ฐ์ง€ ์‹œ: 409 Conflict ๋ฐ˜ํ™˜ (์‚ฌ์šฉ์ž์—๊ฒŒ ์žฌ์‹œ๋„ ์•ˆ๋‚ด) +- Lost Update ๋ฐฉ์ง€: version ํ•„๋“œ ์ž๋™ ์ฆ๊ฐ€ + +**๋ณด์•ˆ ์ฒ˜๋ฆฌ** +- ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ: ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ํ•„์ˆ˜ +- JWT ์ธ์ฆ: Controller์—์„œ @AuthenticationPrincipal๋กœ userId ์ถ”์ถœ +- ๊ถŒํ•œ ๊ฒ€์ฆ: ๋ณธ์ธ๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅ + +**์„ฑ๋Šฅ ๋ชฉํ‘œ** +- ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„: 0.3์ดˆ ์ด๋‚ด +- P95 ์‘๋‹ต ์‹œ๊ฐ„: 0.5์ดˆ ์ด๋‚ด +- ํŠธ๋žœ์žญ์…˜ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€: READ_COMMITTED + +**ํ–ฅํ›„ ๊ฐœ์„ ์‚ฌํ•ญ** +- ์ „ํ™”๋ฒˆํ˜ธ ๋ณ€๊ฒฝ: SMS/์ด๋ฉ”์ผ ์žฌ์ธ์ฆ ๊ตฌํ˜„ +- ์ด๋ฉ”์ผ ๋ณ€๊ฒฝ: ์ด๋ฉ”์ผ ์ธ์ฆ ๊ตฌํ˜„ +- ๋ณ€๊ฒฝ ์ด๋ ฅ ์ถ”์ : Audit Log ๊ธฐ๋ก + +**์—๋Ÿฌ ์ฝ”๋“œ** +- USER_003: ์‚ฌ์šฉ์ž ์—†์Œ +- USER_004: ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ +- USER_005: ๋™์‹œ์„ฑ ์ถฉ๋Œ (๋‹ค๋ฅธ ์„ธ์…˜์—์„œ ์ˆ˜์ •) +end note + +@enduml diff --git a/design/backend/sequence/inner/user-ํšŒ์›๊ฐ€์ž….puml b/design/backend/sequence/inner/user-ํšŒ์›๊ฐ€์ž….puml new file mode 100644 index 0000000..dfd6c71 --- /dev/null +++ b/design/backend/sequence/inner/user-ํšŒ์›๊ฐ€์ž….puml @@ -0,0 +1,149 @@ +@startuml user-ํšŒ์›๊ฐ€์ž… +!theme mono + +title User Service - ํšŒ์›๊ฐ€์ž… ๋‚ด๋ถ€ ์‹œํ€€์Šค (UFR-USER-010) + +participant "UserController" as Controller <> +participant "UserService" as Service <> +participant "UserRepository" as UserRepo <> +participant "StoreRepository" as StoreRepo <> +participant "PasswordEncoder" as PwdEncoder <> +participant "JwtTokenProvider" as JwtProvider <> +participant "Redis\nCache" as Redis <> +participant "User DB\n(PostgreSQL)" as UserDB <> +actor Client + +note over Controller, UserDB +**UFR-USER-010: ํšŒ์›๊ฐ€์ž…** +- ๊ธฐ๋ณธ ์ •๋ณด: ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ +- ๋งค์žฅ ์ •๋ณด: ๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ, ์˜์—…์‹œ๊ฐ„, ์‚ฌ์—…์ž๋ฒˆํ˜ธ +- ์ด๋ฉ”์ผ/์ „ํ™”๋ฒˆํ˜ธ ์ค‘๋ณต ๊ฒ€์‚ฌ +- ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ +- JWT ํ† ํฐ ๋ฐœ๊ธ‰ +end note + +Client -> Controller: POST /api/users/register\n{"name": "ํ™๊ธธ๋™",\n"phoneNumber": "01012345678",\n"email": "hong@example.com",\n"password": "password123"} +activate Controller + +Controller -> Controller: ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ\n(์ด๋ฉ”์ผ ํ˜•์‹, ๋น„๋ฐ€๋ฒˆํ˜ธ 8์ž ์ด์ƒ ๋“ฑ) + +Controller -> Service: register(RegisterRequest) +activate Service + +== 1๋‹จ๊ณ„: ์ด๋ฉ”์ผ ์ค‘๋ณต ํ™•์ธ == + +Service -> UserRepo: findByEmail(email) +activate UserRepo +UserRepo -> UserDB: ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ\n(์ค‘๋ณต ๊ฐ€์ž… ํ™•์ธ) +activate UserDB +UserDB --> UserRepo: ์กฐํšŒ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ๋˜๋Š” ์—†์Œ +deactivate UserDB +UserRepo --> Service: Optional +deactivate UserRepo + +alt ์ด๋ฉ”์ผ ์ค‘๋ณต ์กด์žฌ + Service --> Controller: throw DuplicateEmailException\n("์ด๋ฏธ ๊ฐ€์ž…๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค") + Controller --> Client: 400 Bad Request\n{"code": "USER_001",\n"message": "์ด๋ฏธ ๊ฐ€์ž…๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค"} + deactivate Service + deactivate Controller + +else ์ด๋ฉ”์ผ ์‹ ๊ทœ + + == 2๋‹จ๊ณ„: ์ „ํ™”๋ฒˆํ˜ธ ์ค‘๋ณต ํ™•์ธ == + + Service -> UserRepo: findByPhoneNumber(phoneNumber) + activate UserRepo + UserRepo -> UserDB: ์ „ํ™”๋ฒˆํ˜ธ๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ\n(์ค‘๋ณต ๊ฐ€์ž… ํ™•์ธ) + activate UserDB + UserDB --> UserRepo: ์กฐํšŒ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ๋˜๋Š” ์—†์Œ + deactivate UserDB + UserRepo --> Service: Optional + deactivate UserRepo + + alt ์ „ํ™”๋ฒˆํ˜ธ ์ค‘๋ณต ์กด์žฌ + Service --> Controller: throw DuplicatePhoneException\n("์ด๋ฏธ ๊ฐ€์ž…๋œ ์ „ํ™”๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค") + Controller --> Client: 400 Bad Request\n{"code": "USER_002",\n"message": "์ด๋ฏธ ๊ฐ€์ž…๋œ ์ „ํ™”๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค"} + deactivate Service + deactivate Controller + + else ์‹ ๊ทœ ์‚ฌ์šฉ์ž + + == 3๋‹จ๊ณ„: ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ == + + Service -> PwdEncoder: encode(rawPassword) + activate PwdEncoder + PwdEncoder -> PwdEncoder: bcrypt ํ•ด์‹ฑ\n(Cost Factor 10) + PwdEncoder --> Service: passwordHash + deactivate PwdEncoder + + == 4๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŠธ๋žœ์žญ์…˜ == + + Service -> UserDB: ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ + activate UserDB + + Service -> UserRepo: save(User) + activate UserRepo + UserRepo -> UserDB: ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ\n(์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ,\n๋น„๋ฐ€๋ฒˆํ˜ธํ•ด์‹œ, ์ƒ์„ฑ์ผ์‹œ)\n์ €์žฅ ํ›„ ์‚ฌ์šฉ์žID ๋ฐ˜ํ™˜ + UserDB --> UserRepo: ์ƒ์„ฑ๋œ ์‚ฌ์šฉ์žID + UserRepo --> Service: User ์—”ํ‹ฐํ‹ฐ\n(userId ํฌํ•จ) + deactivate UserRepo + + Service -> StoreRepo: save(Store) + activate StoreRepo + StoreRepo -> UserDB: ๋งค์žฅ ์ •๋ณด ์ €์žฅ\n(์‚ฌ์šฉ์žID, ๋งค์žฅ๋ช…, ์—…์ข…,\n์ฃผ์†Œ, ์‚ฌ์—…์ž๋ฒˆํ˜ธ, ์˜์—…์‹œ๊ฐ„)\n์ €์žฅ ํ›„ ๋งค์žฅID ๋ฐ˜ํ™˜ + UserDB --> StoreRepo: ์ƒ์„ฑ๋œ ๋งค์žฅID + StoreRepo --> Service: Store ์—”ํ‹ฐํ‹ฐ\n(storeId ํฌํ•จ) + deactivate StoreRepo + + Service -> UserDB: ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ + UserDB --> Service: ์ปค๋ฐ‹ ์™„๋ฃŒ + deactivate UserDB + + == 5๋‹จ๊ณ„: JWT ํ† ํฐ ์ƒ์„ฑ == + + Service -> JwtProvider: generateToken(userId, role) + activate JwtProvider + JwtProvider -> JwtProvider: JWT ํ† ํฐ ์ƒ์„ฑ\n(Claims: userId, role=OWNER,\nexp=7์ผ) + JwtProvider --> Service: JWT ํ† ํฐ + deactivate JwtProvider + + == 6๋‹จ๊ณ„: ์„ธ์…˜ ์ €์žฅ == + + Service -> Redis: ์„ธ์…˜ ์ •๋ณด ์ €์žฅ\nKey: user:session:{token}\nValue: {userId, role}\nTTL: 7์ผ + activate Redis + Redis --> Service: ์ €์žฅ ์™„๋ฃŒ + deactivate Redis + + == 7๋‹จ๊ณ„: ์‘๋‹ต ๋ฐ˜ํ™˜ == + + Service -> Service: ํšŒ์›๊ฐ€์ž… ์‘๋‹ต DTO ์ƒ์„ฑ + Service --> Controller: RegisterResponse\n{token, userId, userName, storeId, storeName} + deactivate Service + + Controller --> Client: 201 Created\n{"token": "eyJhbGc...",\n"userId": 123,\n"userName": "ํ™๊ธธ๋™",\n"storeId": 456,\n"storeName": "๋ง›์žˆ๋Š”์ง‘"} + deactivate Controller + end + end +end + +note over Controller, UserDB +**Transaction Rollback ์ฒ˜๋ฆฌ** +- ํŠธ๋žœ์žญ์…˜ ์‹คํŒจ ์‹œ ์ž๋™ Rollback +- User/Store INSERT ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจ ์‹œ ์ „์ฒด ๋กค๋ฐฑ +- ์˜ˆ์™ธ: DataAccessException, ConstraintViolationException + +**๋ณด์•ˆ ์ฒ˜๋ฆฌ** +- ๋น„๋ฐ€๋ฒˆํ˜ธ: bcrypt ํ•ด์‹ฑ (Cost Factor 10) +- JWT ํ† ํฐ: 7์ผ ๋งŒ๋ฃŒ, ์„œ๋ฒ„ ์„ธ์…˜๊ณผ ๋™๊ธฐํ™” + +**์„ฑ๋Šฅ ๋ชฉํ‘œ** +- ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„: 1.0์ดˆ ์ด๋‚ด +- P95 ์‘๋‹ต ์‹œ๊ฐ„: 1.5์ดˆ ์ด๋‚ด +- ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ: 0.5์ดˆ ์ด๋‚ด + +**์—๋Ÿฌ ์ฝ”๋“œ** +- USER_001: ์ด๋ฉ”์ผ ์ค‘๋ณต +- USER_002: ์ „ํ™”๋ฒˆํ˜ธ ์ค‘๋ณต +end note + +@enduml diff --git a/design/backend/sequence/outer/README.md b/design/backend/sequence/outer/README.md new file mode 100644 index 0000000..99a1be5 --- /dev/null +++ b/design/backend/sequence/outer/README.md @@ -0,0 +1,304 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ์™ธ๋ถ€ ์‹œํ€€์Šค ์„ค๊ณ„ + +## ๋ฌธ์„œ ์ •๋ณด +- **์ž‘์„ฑ์ผ**: 2025-10-22 +- **์ž‘์„ฑ์ž**: System Architect +- **๋ฒ„์ „**: 1.0 +- **๊ด€๋ จ ๋ฌธ์„œ**: + - [์œ ์ €์Šคํ† ๋ฆฌ](../../../userstory.md) + - [๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜](../../logical/logical-architecture.md) + - [UI/UX ์„ค๊ณ„์„œ](../../../uiux/uiux.md) + +--- + +## ๊ฐœ์š” + +๋ณธ ๋ฌธ์„œ๋Š” KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค์˜ **์™ธ๋ถ€ ์‹œํ€€์Šค ์„ค๊ณ„**๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. +์™ธ๋ถ€ ์‹œํ€€์Šค๋Š” ์„œ๋น„์Šค ๊ฐ„์˜ ์ƒํ˜ธ์ž‘์šฉ๊ณผ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ํ‘œํ˜„ํ•˜๋ฉฐ, ์œ ์ €์Šคํ† ๋ฆฌ์™€ ๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + +### ์„ค๊ณ„ ์›์น™ +1. **์œ ์ €์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜**: 20๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ์™€ ์ •ํ™•ํžˆ ๋งค์นญ +2. **Event-Driven ์•„ํ‚คํ…์ฒ˜**: Kafka๋ฅผ ํ†ตํ•œ ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ ๋ฐœํ–‰/๊ตฌ๋… +3. **Resilience ํŒจํ„ด**: Circuit Breaker, Retry, Timeout, Fallback ์ ์šฉ +4. **Cache-Aside ํŒจํ„ด**: Redis ์บ์‹ฑ์„ ํ†ตํ•œ ์„ฑ๋Šฅ ์ตœ์ ํ™” +5. **์„œ๋น„์Šค ๋…๋ฆฝ์„ฑ**: ๋А์Šจํ•œ ๊ฒฐํ•ฉ๊ณผ ์žฅ์•  ๊ฒฉ๋ฆฌ + +--- + +## ์™ธ๋ถ€ ์‹œํ€€์Šค ํ”Œ๋กœ์šฐ ๋ชฉ๋ก + +์ด **4๊ฐœ์˜ ์ฃผ์š” ๋น„์ฆˆ๋‹ˆ์Šค ํ”Œ๋กœ์šฐ**๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค: + +### 1. ์‚ฌ์šฉ์ž ์ธ์ฆ ํ”Œ๋กœ์šฐ +**ํŒŒ์ผ**: `์‚ฌ์šฉ์ž์ธ์ฆํ”Œ๋กœ์šฐ.puml` + +**ํฌํ•จ๋œ ์œ ์ €์Šคํ† ๋ฆฌ**: +- UFR-USER-010: ํšŒ์›๊ฐ€์ž… +- UFR-USER-020: ๋กœ๊ทธ์ธ +- UFR-USER-040: ๋กœ๊ทธ์•„์›ƒ + +**์ฃผ์š” ์ฐธ์—ฌ์ž**: +- Frontend (Web/Mobile) +- API Gateway +- User Service +- Redis Cache +- User DB (PostgreSQL) +- ๊ตญ์„ธ์ฒญ API (์™ธ๋ถ€) + +**ํ•ต์‹ฌ ๊ธฐ๋Šฅ**: +- JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ (Circuit Breaker ์ ์šฉ) +- Redis ์บ์‹ฑ (์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ๊ฒฐ๊ณผ, TTL 7์ผ) +- ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ (bcrypt) +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ์•”ํ˜ธํ™” (AES-256) + +**Resilience ํŒจํ„ด**: +- Circuit Breaker: ๊ตญ์„ธ์ฒญ API (์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open) +- Retry: ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„ (์ง€์ˆ˜ ๋ฐฑ์˜คํ”„: 1์ดˆ, 2์ดˆ, 4์ดˆ) +- Timeout: 5์ดˆ +- Fallback: ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ์Šคํ‚ต (์ˆ˜๋™ ํ™•์ธ ์•ˆ๋‚ด) + +--- + +### 2. ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ +**ํŒŒ์ผ**: `์ด๋ฒคํŠธ์ƒ์„ฑํ”Œ๋กœ์šฐ.puml` + +**ํฌํ•จ๋œ ์œ ์ €์Šคํ† ๋ฆฌ**: +- UFR-EVENT-020: ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ +- UFR-EVENT-030: AI ์ด๋ฒคํŠธ ์ถ”์ฒœ +- UFR-CONT-010: SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ +- UFR-EVENT-050: ์ตœ์ข… ์Šน์ธ ๋ฐ ๋ฐฐํฌ + +**์ฃผ์š” ์ฐธ์—ฌ์ž**: +- Frontend +- API Gateway +- Event Service +- AI Service (Kafka ๊ตฌ๋…) +- Content Service (Kafka ๊ตฌ๋…) +- Distribution Service (๋™๊ธฐ ํ˜ธ์ถœ) +- Kafka (Event Topics + Job Topics) +- Redis Cache +- Event DB +- ์™ธ๋ถ€ API (AI API, ์ด๋ฏธ์ง€ ์ƒ์„ฑ API, ๋ฐฐํฌ ์ฑ„๋„ APIs) + +**ํ•ต์‹ฌ ๊ธฐ๋Šฅ**: +1. **์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ** (๋™๊ธฐ) + - Event DB์— ๋ชฉ์  ์ €์žฅ + - EventCreated ์ด๋ฒคํŠธ ๋ฐœํ–‰ + +2. **AI ์ด๋ฒคํŠธ ์ถ”์ฒœ** (๋น„๋™๊ธฐ) + - Kafka ai-job ํ† ํ”ฝ ๋ฐœํ–‰ + - AI Service ๊ตฌ๋… ๋ฐ ์ฒ˜๋ฆฌ + - Polling ํŒจํ„ด์œผ๋กœ Job ์ƒํƒœ ํ™•์ธ (์ตœ๋Œ€ 30์ดˆ) + - Redis ์บ์‹ฑ (TTL 24์‹œ๊ฐ„) + +3. **SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ** (๋น„๋™๊ธฐ) + - Kafka image-job ํ† ํ”ฝ ๋ฐœํ–‰ + - Content Service ๊ตฌ๋… ๋ฐ ์ฒ˜๋ฆฌ + - Polling ํŒจํ„ด์œผ๋กœ Job ์ƒํƒœ ํ™•์ธ (์ตœ๋Œ€ 20์ดˆ) + - CDN ์—…๋กœ๋“œ ๋ฐ Redis ์บ์‹ฑ (TTL 7์ผ) + +4. **์ตœ์ข… ์Šน์ธ ๋ฐ ๋ฐฐํฌ** (๋™๊ธฐ) + - Distribution Service REST API ์ง์ ‘ ํ˜ธ์ถœ + - ๋‹ค์ค‘ ์ฑ„๋„ ๋ณ‘๋ ฌ ๋ฐฐํฌ (1๋ถ„ ์ด๋‚ด) + - DistributionCompleted ์ด๋ฒคํŠธ ๋ฐœํ–‰ + +**Resilience ํŒจํ„ด**: +- Circuit Breaker: ๋ชจ๋“  ์™ธ๋ถ€ API ํ˜ธ์ถœ ์‹œ ์ ์šฉ +- Retry: ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„ (์ง€์ˆ˜ ๋ฐฑ์˜คํ”„) +- Timeout: AI API 30์ดˆ, ์ด๋ฏธ์ง€ API 20์ดˆ, ๋ฐฐํฌ API 10์ดˆ +- Bulkhead: ์ฑ„๋„๋ณ„ ์Šค๋ ˆ๋“œ ํ’€ ๊ฒฉ๋ฆฌ +- Fallback: AI ์ถ”์ฒœ ์‹œ ์บ์‹œ๋œ ์ด์ „ ๊ฒฐ๊ณผ, ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ + +--- + +### 3. ๊ณ ๊ฐ ์ฐธ์—ฌ ํ”Œ๋กœ์šฐ +**ํŒŒ์ผ**: `๊ณ ๊ฐ์ฐธ์—ฌํ”Œ๋กœ์šฐ.puml` + +**ํฌํ•จ๋œ ์œ ์ €์Šคํ† ๋ฆฌ**: +- UFR-PART-010: ์ด๋ฒคํŠธ ์ฐธ์—ฌ +- UFR-PART-030: ๋‹น์ฒจ์ž ์ถ”์ฒจ + +**์ฃผ์š” ์ฐธ์—ฌ์ž**: +- Frontend (๊ณ ๊ฐ์šฉ / ์‚ฌ์žฅ๋‹˜์šฉ) +- API Gateway +- Participation Service +- Kafka (Event Topics) +- Participation DB +- Analytics Service (์ด๋ฒคํŠธ ๊ตฌ๋…) + +**ํ•ต์‹ฌ ๊ธฐ๋Šฅ**: +1. **์ด๋ฒคํŠธ ์ฐธ์—ฌ** + - ์ค‘๋ณต ์ฐธ์—ฌ ์ฒดํฌ (์ „ํ™”๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜) + - ์‘๋ชจ ๋ฒˆํ˜ธ ๋ฐœ๊ธ‰ + - ParticipantRegistered ์ด๋ฒคํŠธ ๋ฐœํ–‰ โ†’ Analytics Service ๊ตฌ๋… + +2. **๋‹น์ฒจ์ž ์ถ”์ฒจ** + - ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฌด์ž‘์œ„ ์ถ”์ฒจ (Crypto.randomBytes) + - Fisher-Yates Shuffle ์•Œ๊ณ ๋ฆฌ์ฆ˜ + - ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์  ์ ์šฉ (์„ ํƒ ์˜ต์…˜) + - WinnerSelected ์ด๋ฒคํŠธ ๋ฐœํ–‰ + +**Event-Driven ํŠน์ง•**: +- Analytics Service๊ฐ€ ParticipantRegistered ์ด๋ฒคํŠธ ๊ตฌ๋…ํ•˜์—ฌ ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ +- ์„œ๋น„์Šค ๊ฐ„ ์ง์ ‘ ์˜์กด์„ฑ ์—†์ด ์ด๋ฒคํŠธ๋กœ ๋А์Šจํ•œ ๊ฒฐํ•ฉ + +--- + +### 4. ์„ฑ๊ณผ ๋ถ„์„ ํ”Œ๋กœ์šฐ +**ํŒŒ์ผ**: `์„ฑ๊ณผ๋ถ„์„ํ”Œ๋กœ์šฐ.puml` + +**ํฌํ•จ๋œ ์œ ์ €์Šคํ† ๋ฆฌ**: +- UFR-ANAL-010: ์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ + +**์ฃผ์š” ์ฐธ์—ฌ์ž**: +- Frontend +- API Gateway +- Analytics Service +- Redis Cache (TTL 5๋ถ„) +- Analytics DB +- Kafka (Event Topics ๊ตฌ๋…) +- ์™ธ๋ถ€ API (์šฐ๋ฆฌ๋™๋„คTV, ์ง€๋‹ˆTV, SNS APIs) + +**ํ•ต์‹ฌ ๊ธฐ๋Šฅ**: +1. **๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ - Cache HIT** (0.5์ดˆ) + - Redis์—์„œ ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + - ํžˆํŠธ์œจ ๋ชฉํ‘œ: 95% + +2. **๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ - Cache MISS** (3์ดˆ) + - Analytics DB ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ์กฐํšŒ + - ์™ธ๋ถ€ ์ฑ„๋„ API ๋ณ‘๋ ฌ ํ˜ธ์ถœ (Circuit Breaker ์ ์šฉ) + - ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ๋ฐ ROI ๊ณ„์‚ฐ + - Redis ์บ์‹ฑ (TTL 5๋ถ„) + +3. **์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (Background)** + - EventCreated ๊ตฌ๋…: ์ด๋ฒคํŠธ ๊ธฐ๋ณธ ์ •๋ณด ์ดˆ๊ธฐํ™” + - ParticipantRegistered ๊ตฌ๋…: ์ฐธ์—ฌ์ž ์ˆ˜ ์‹ค์‹œ๊ฐ„ ์ฆ๊ฐ€ + - DistributionCompleted ๊ตฌ๋…: ๋ฐฐํฌ ์ฑ„๋„ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ + +**Resilience ํŒจํ„ด**: +- Circuit Breaker: ์™ธ๋ถ€ ์ฑ„๋„ API ์กฐํšŒ ์‹œ (์‹คํŒจ์œจ 50% ์ดˆ๊ณผ ์‹œ Open) +- Timeout: 10์ดˆ +- Fallback: ์บ์‹œ๋œ ์ด์ „ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • +- ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ: ์™ธ๋ถ€ ์ฑ„๋„ API ๋™์‹œ ํ˜ธ์ถœ + +--- + +## ์„ค๊ณ„ ํŠน์ง• + +### 1. Event-Driven ์•„ํ‚คํ…์ฒ˜ +- **Kafka ํ†ตํ•ฉ**: Event Topics์™€ Job Topics๋ฅผ Kafka๋กœ ํ†ตํ•ฉ +- **๋А์Šจํ•œ ๊ฒฐํ•ฉ**: ์„œ๋น„์Šค ๊ฐ„ ์ง์ ‘ ์˜์กด์„ฑ ์ œ๊ฑฐ +- **์žฅ์•  ๊ฒฉ๋ฆฌ**: ํ•œ ์„œ๋น„์Šค ์žฅ์• ๊ฐ€ ๋‹ค๋ฅธ ์„œ๋น„์Šค์— ์˜ํ–ฅ ์—†์Œ +- **ํ™•์žฅ ์šฉ์ด**: ์ƒˆ๋กœ์šด ๊ตฌ๋…์ž ์ถ”๊ฐ€๋กœ ๊ธฐ๋Šฅ ํ™•์žฅ + +### 2. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ํŒจํ„ด +- **Kafka Job Topics**: ai-job, image-job +- **Polling ํŒจํ„ด**: Job ์ƒํƒœ ํ™•์ธ (2-5์ดˆ ๊ฐ„๊ฒฉ) +- **์ฒ˜๋ฆฌ ์‹œ๊ฐ„**: AI ์ถ”์ฒœ 10์ดˆ, ์ด๋ฏธ์ง€ ์ƒ์„ฑ 5์ดˆ + +### 3. ๋™๊ธฐ ์ฒ˜๋ฆฌ ํŒจํ„ด +- **Distribution Service**: REST API ์ง์ ‘ ํ˜ธ์ถœ +- **๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ**: ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ (1๋ถ„ ์ด๋‚ด) +- **Circuit Breaker**: ์žฅ์•  ์ „ํŒŒ ๋ฐฉ์ง€ + +### 4. Resilience ํŒจํ„ด ์ „๋ฉด ์ ์šฉ +- **Circuit Breaker**: ๋ชจ๋“  ์™ธ๋ถ€ API ํ˜ธ์ถœ +- **Retry**: ์ผ์‹œ์  ์žฅ์•  ์ž๋™ ๋ณต๊ตฌ +- **Timeout**: ์‘๋‹ต ์‹œ๊ฐ„ ์ œํ•œ +- **Bulkhead**: ๋ฆฌ์†Œ์Šค ๊ฒฉ๋ฆฌ +- **Fallback**: ์žฅ์•  ์‹œ ๋Œ€์ฒด ๋กœ์ง + +### 5. Cache-Aside ํŒจํ„ด +- **Redis ์บ์‹ฑ**: ์„ฑ๋Šฅ ์ตœ์ ํ™” +- **TTL ์„ค์ •**: ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ด€๋ฆฌ +- **ํžˆํŠธ์œจ ๋ชฉํ‘œ**: 80-95% +- **์‘๋‹ต ์‹œ๊ฐ„ ๊ฐœ์„ **: 90-99% + +--- + +## ํŒŒ์ผ ๊ตฌ์กฐ + +``` +design/backend/sequence/outer/ +โ”œโ”€โ”€ README.md (๋ณธ ๋ฌธ์„œ) +โ”œโ”€โ”€ ์‚ฌ์šฉ์ž์ธ์ฆํ”Œ๋กœ์šฐ.puml +โ”œโ”€โ”€ ์ด๋ฒคํŠธ์ƒ์„ฑํ”Œ๋กœ์šฐ.puml +โ”œโ”€โ”€ ๊ณ ๊ฐ์ฐธ์—ฌํ”Œ๋กœ์šฐ.puml +โ””โ”€โ”€ ์„ฑ๊ณผ๋ถ„์„ํ”Œ๋กœ์šฐ.puml +``` + +--- + +## ๋‹ค์ด์–ด๊ทธ๋žจ ํ™•์ธ ๋ฐฉ๋ฒ• + +### 1. Online PlantUML Viewer +1. https://www.plantuml.com/plantuml/uml ์ ‘์† +2. `.puml` ํŒŒ์ผ ๋‚ด์šฉ ๋ถ™์—ฌ๋„ฃ๊ธฐ +3. ๋‹ค์ด์–ด๊ทธ๋žจ ์‹œ๊ฐ์  ํ™•์ธ + +### 2. VSCode Extension +1. "PlantUML" ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ์„ค์น˜ +2. `.puml` ํŒŒ์ผ ์—ด๊ธฐ +3. `Alt+D` ๋˜๋Š” `Cmd+D`๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ + +### 3. IntelliJ IDEA Plugin +1. "PlantUML integration" ํ”Œ๋Ÿฌ๊ทธ์ธ ์„ค์น˜ +2. `.puml` ํŒŒ์ผ ์—ด๊ธฐ +3. ์šฐ์ธก ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํŒจ๋„์—์„œ ํ™•์ธ + +--- + +## ์ฃผ์š” ๊ฒฐ์ •์‚ฌํ•ญ + +1. **Kafka ํ†ตํ•ฉ**: Event Bus์™€ Job Queue๋ฅผ Kafka๋กœ ํ†ตํ•ฉํ•˜์—ฌ ์šด์˜ ๋ณต์žก๋„ ๊ฐ์†Œ +2. **๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ**: AI ์ถ”์ฒœ ๋ฐ ์ด๋ฏธ์ง€ ์ƒ์„ฑ์€ Kafka Job Topics๋ฅผ ํ†ตํ•œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ +3. **๋™๊ธฐ ๋ฐฐํฌ**: Distribution Service๋Š” REST API ์ง์ ‘ ํ˜ธ์ถœํ•˜์—ฌ ๋™๊ธฐ ์ฒ˜๋ฆฌ (1๋ถ„ ์ด๋‚ด) +4. **Resilience ํŒจํ„ด**: ๋ชจ๋“  ์™ธ๋ถ€ API ํ˜ธ์ถœ ์‹œ Circuit Breaker, Retry, Timeout, Fallback ์ ์šฉ +5. **Cache-Aside ํŒจํ„ด**: Redis ์บ์‹ฑ์œผ๋กœ ์‘๋‹ต ์‹œ๊ฐ„ 90-99% ๊ฐœ์„  +6. **Event Topics**: EventCreated, ParticipantRegistered, WinnerSelected, DistributionCompleted +7. **Job Topics**: ai-job, image-job + +--- + +## ๊ฒ€์ฆ ์‚ฌํ•ญ + +### 1. ์œ ์ €์Šคํ† ๋ฆฌ ๋งค์นญ +โœ… ๋ชจ๋“  ์œ ์ €์Šคํ† ๋ฆฌ๊ฐ€ ์™ธ๋ถ€ ์‹œํ€€์Šค์— ์ •ํ™•ํžˆ ๋ฐ˜์˜๋จ +- User ์„œ๋น„์Šค: 4๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ +- Event ์„œ๋น„์Šค: 4๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ +- Participation ์„œ๋น„์Šค: 2๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ +- Analytics ์„œ๋น„์Šค: 1๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ + +### 2. ๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜ ์ผ์น˜์„ฑ +โœ… ๋…ผ๋ฆฌ ์•„ํ‚คํ…์ฒ˜์˜ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์™€ ํ†ต์‹  ํŒจํ„ด ๋ฐ˜์˜ +- Core Services: User, Event, Participation, Analytics +- Async Services: AI, Content, Distribution +- Kafka: Event Topics + Job Topics +- External Systems: ๊ตญ์„ธ์ฒญ API, AI API, ์ด๋ฏธ์ง€ ์ƒ์„ฑ API, ๋ฐฐํฌ ์ฑ„๋„ APIs + +### 3. Resilience ํŒจํ„ด ์ ์šฉ +โœ… ๋ชจ๋“  ์™ธ๋ถ€ API ํ˜ธ์ถœ์— Resilience ํŒจํ„ด ์ ์šฉ +- Circuit Breaker, Retry, Timeout, Bulkhead, Fallback + +### 4. PlantUML ๋ฌธ๋ฒ• ๊ฒ€์ฆ +โœ… PlantUML ๊ธฐ๋ณธ ๋ฌธ๋ฒ• ๊ฒ€์ฆ ์™„๋ฃŒ +- `!theme mono` ์ ์šฉ +- ๋™๊ธฐ/๋น„๋™๊ธฐ ํ™”์‚ดํ‘œ ๊ตฌ๋ถ„ +- ํ•œ๊ธ€ ์„ค๋ช… ์ถ”๊ฐ€ +- ์ฐธ์—ฌ์ž ๋ฐ ํ”Œ๋กœ์šฐ ๋ช…ํ™•ํžˆ ํ‘œํ˜„ + +--- + +## ํ–ฅํ›„ ๊ฐœ์„  ๋ฐฉ์•ˆ + +1. **WebSocket ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ํ‘ธ์‹œ**: ๋Œ€์‹œ๋ณด๋“œ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (ํด๋ง ๋Œ€์ฒด) +2. **Saga ํŒจํ„ด ์ ์šฉ**: ๋ณต์žกํ•œ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ๋ณด์ƒ ๋กœ์ง ์ฒด๊ณ„ํ™” +3. **Service Mesh ๋„์ž…**: Istio๋ฅผ ํ†ตํ•œ ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  ๊ด€์ฐฐ์„ฑ ๋ฐ ๋ณด์•ˆ ๊ฐ•ํ™” +4. **Dead Letter Queue ๊ณ ๋„ํ™”**: ์‹คํŒจ ์ด๋ฒคํŠธ ์žฌ์ฒ˜๋ฆฌ ๋ฐ ์•Œ๋ฆผ ์ž๋™ํ™” + +--- + +**๋ฌธ์„œ ๋ฒ„์ „**: 1.0 +**์ตœ์ข… ์ˆ˜์ •์ผ**: 2025-10-22 +**์ž‘์„ฑ์ž**: System Architect diff --git a/design/backend/sequence/outer/๊ณ ๊ฐ์ฐธ์—ฌํ”Œ๋กœ์šฐ.puml b/design/backend/sequence/outer/๊ณ ๊ฐ์ฐธ์—ฌํ”Œ๋กœ์šฐ.puml new file mode 100644 index 0000000..a942530 --- /dev/null +++ b/design/backend/sequence/outer/๊ณ ๊ฐ์ฐธ์—ฌํ”Œ๋กœ์šฐ.puml @@ -0,0 +1,164 @@ +@startuml ๊ณ ๊ฐ์ฐธ์—ฌํ”Œ๋กœ์šฐ +!theme mono + +title ๊ณ ๊ฐ ์ฐธ์—ฌ ํ”Œ๋กœ์šฐ - ์™ธ๋ถ€ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ + +actor "๊ณ ๊ฐ" as Customer +participant "Frontend\n(๊ณ ๊ฐ์šฉ)" as CustomerFE +participant "API Gateway" as Gateway +participant "Participation\nService" as PartService +participant "Kafka\n(Event Topics)" as Kafka +database "Participation\nDB" as PartDB +participant "Analytics\nService" as Analytics + +actor "์‚ฌ์žฅ๋‹˜" as Owner +participant "Frontend\n(์‚ฌ์žฅ๋‹˜์šฉ)" as OwnerFE + +== UFR-PART-010: ์ด๋ฒคํŠธ ์ฐธ์—ฌ == + +Customer -> CustomerFE: ์ด๋ฒคํŠธ ์ฐธ์—ฌ ํ™”๋ฉด ์ ‘๊ทผ\n(์šฐ๋ฆฌ๋™๋„คTV/SNS/๋ง๊ณ ๋น„์ฆˆ) +activate CustomerFE + +CustomerFE -> Customer: ์ฐธ์—ฌ ์ •๋ณด ์ž…๋ ฅ ํผ ํ‘œ์‹œ\n(์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ฐธ์—ฌ๊ฒฝ๋กœ) + +Customer -> CustomerFE: ์ฐธ์—ฌ ์ •๋ณด ์ž…๋ ฅ ๋ฐ\n์ฐธ์—ฌ ๋ฒ„ํŠผ ํด๋ฆญ +CustomerFE -> CustomerFE: ํด๋ผ์ด์–ธํŠธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ\n(์ด๋ฆ„ 2์ž ์ด์ƒ, ์ „ํ™”๋ฒˆํ˜ธ ํ˜•์‹) + +CustomerFE -> Gateway: POST /api/v1/participations\n{์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ฐธ์—ฌ๊ฒฝ๋กœ, ๊ฐœ์ธ์ •๋ณด๋™์˜} +activate Gateway + +Gateway -> PartService: POST /participations/register\n{์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ฐธ์—ฌ๊ฒฝ๋กœ, ๊ฐœ์ธ์ •๋ณด๋™์˜} +activate PartService + +PartService -> PartDB: ์ฐธ์—ฌ์ž ์ค‘๋ณต ํ™•์ธ\n(์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฒคํŠธID๋กœ ์กฐํšŒ) +activate PartDB +PartDB --> PartService: ์ค‘๋ณต ์ฐธ์—ฌ ์—ฌ๋ถ€ ๋ฐ˜ํ™˜ +deactivate PartDB + +alt ์ค‘๋ณต ์ฐธ์—ฌ์ธ ๊ฒฝ์šฐ + PartService --> Gateway: 409 Conflict\n{message: "์ด๋ฏธ ์ฐธ์—ฌํ•˜์‹  ์ด๋ฒคํŠธ์ž…๋‹ˆ๋‹ค"} + Gateway --> CustomerFE: 409 Conflict + CustomerFE -> Customer: ์ค‘๋ณต ์ฐธ์—ฌ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + deactivate PartService + deactivate Gateway + deactivate CustomerFE +else ์‹ ๊ทœ ์ฐธ์—ฌ์ธ ๊ฒฝ์šฐ + PartService -> PartService: ์‘๋ชจ ๋ฒˆํ˜ธ ์ƒ์„ฑ\n(UUID ๋˜๋Š” ์‹œํ€€์Šค ๊ธฐ๋ฐ˜) + + PartService -> PartDB: ์ฐธ์—ฌ์ž ์ •๋ณด ์ €์žฅ\n(์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ฐธ์—ฌ๊ฒฝ๋กœ,\n์‘๋ชจ๋ฒˆํ˜ธ, ์ฐธ์—ฌ์ผ์‹œ) + activate PartDB + PartDB --> PartService: ์ €์žฅ ์™„๋ฃŒ + deactivate PartDB + + PartService -> Kafka: Publish Event\n"ParticipantRegistered"\n{participantId, eventId,\nentryPath, timestamp} + activate Kafka + note right of Kafka + Topic: participant-events + Event: ParticipantRegistered + Data: { + participantId: UUID, + eventId: UUID, + entryPath: string, + timestamp: datetime + } + end note + + Kafka --> Analytics: Subscribe Event\n"ParticipantRegistered" + activate Analytics + Analytics -> Analytics: ์ฐธ์—ฌ์ž ๋ฐ์ดํ„ฐ ์ง‘๊ณ„\n- ์ฑ„๋„๋ณ„ ์ฐธ์—ฌ์ž ์ˆ˜\n- ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด\n- ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ + deactivate Analytics + deactivate Kafka + + PartService --> Gateway: 201 Created\n{์‘๋ชจ๋ฒˆํ˜ธ, ๋‹น์ฒจ๋ฐœํ‘œ์ผ, ์ฐธ์—ฌ์™„๋ฃŒ๋ฉ”์‹œ์ง€} + deactivate PartService + + Gateway --> CustomerFE: 201 Created + deactivate Gateway + + CustomerFE -> Customer: ์ฐธ์—ฌ ์™„๋ฃŒ ํ™”๋ฉด ํ‘œ์‹œ\n- ์‘๋ชจ๋ฒˆํ˜ธ\n- ๋‹น์ฒจ ๋ฐœํ‘œ์ผ\n- "์ฐธ์—ฌํ•ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค" + deactivate CustomerFE +end + +== UFR-PART-020: ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ == + +Owner -> OwnerFE: ์ด๋ฒคํŠธ ์ƒ์„ธ ํ™”๋ฉด์—์„œ\n"์ฐธ์—ฌ์ž ๋ชฉ๋ก" ํƒญ ํด๋ฆญ +activate OwnerFE + +OwnerFE -> Gateway: GET /api/v1/events/{eventId}/participants\n?page=1&size=20 +activate Gateway + +Gateway -> PartService: GET /events/{eventId}/participants\n?page=1&size=20 +activate PartService + +PartService -> PartDB: ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ\n(์ด๋ฒคํŠธID, ํŽ˜์ด์ง€๋„ค์ด์…˜)\nORDER BY ์ฐธ์—ฌ์ผ์‹œ DESC +activate PartDB +PartDB --> PartService: ์ฐธ์—ฌ์ž ๋ชฉ๋ก ๋ฐ˜ํ™˜\n(์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ฐธ์—ฌ๊ฒฝ๋กœ,\n์‘๋ชจ๋ฒˆํ˜ธ, ์ฐธ์—ฌ์ผ์‹œ)\n+ ์ด ์ฐธ์—ฌ์ž ์ˆ˜ +deactivate PartDB + +PartService --> Gateway: 200 OK\n{participants[], totalCount, page, size} +deactivate PartService + +Gateway --> OwnerFE: 200 OK +deactivate Gateway + +OwnerFE -> Owner: ์ฐธ์—ฌ์ž ๋ชฉ๋ก ํ™”๋ฉด ํ‘œ์‹œ\n- ์ฐธ์—ฌ์ž ์ •๋ณด ํ…Œ์ด๋ธ”\n- ํŽ˜์ด์ง€๋„ค์ด์…˜\n- ์ด ์ฐธ์—ฌ์ž ์ˆ˜\n- CSV ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ +deactivate OwnerFE + +note right of Owner + ์ฐธ์—ฌ์ž ์ •๋ณด: + - ์ด๋ฆ„ (๋งˆ์Šคํ‚น: ๊น€**) + - ์ „ํ™”๋ฒˆํ˜ธ (๋งˆ์Šคํ‚น: 010-****-1234) + - ์ฐธ์—ฌ๊ฒฝ๋กœ (์šฐ๋ฆฌ๋™๋„คTV, Instagram ๋“ฑ) + - ์‘๋ชจ๋ฒˆํ˜ธ + - ์ฐธ์—ฌ์ผ์‹œ +end note + +== UFR-PART-030: ๋‹น์ฒจ์ž ์ถ”์ฒจ == + +Owner -> OwnerFE: ์ด๋ฒคํŠธ ์ƒ์„ธ ํ™”๋ฉด์—์„œ\n"๋‹น์ฒจ์ž ์ถ”์ฒจ" ๋ฒ„ํŠผ ํด๋ฆญ +activate OwnerFE + +OwnerFE -> Owner: ์ถ”์ฒจ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ\n"๋‹น์ฒจ์ž๋ฅผ ์ถ”์ฒจํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + +Owner -> OwnerFE: ํ™•์ธ ๋ฒ„ํŠผ ํด๋ฆญ + +OwnerFE -> Gateway: POST /api/v1/events/{eventId}/draw-winners\n{๋‹น์ฒจ์ธ์›, ๋งค์žฅ๋ฐฉ๋ฌธ๊ฐ€์‚ฐ์ ์˜ต์…˜} +activate Gateway + +Gateway -> PartService: POST /events/{eventId}/draw-winners\n{winnerCount, visitBonus} +activate PartService + +PartService -> PartDB: ๋ฏธ๋‹น์ฒจ ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ\n(์ด๋ฒคํŠธID๋กœ ๋‹น์ฒจ๋˜์ง€ ์•Š์€ ์ฐธ์—ฌ์ž ์กฐํšŒ) +activate PartDB +PartDB --> PartService: ์ „์ฒด ์ฐธ์—ฌ์ž ๋ชฉ๋ก ๋ฐ˜ํ™˜ +deactivate PartDB + +PartService -> PartService: ๋‹น์ฒจ์ž ์ถ”์ฒจ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์‹คํ–‰\n1. ๋‚œ์ˆ˜ ์ƒ์„ฑ (Crypto.randomBytes)\n2. ๋งค์žฅ๋ฐฉ๋ฌธ ๊ฐ€์‚ฐ์  ์ ์šฉ (์˜ต์…˜)\n3. Fisher-Yates Shuffle\n4. ๋‹น์ฒจ์ธ์›๋งŒํผ ์„ ์ • + +PartService -> PartDB: ๋‹น์ฒจ์ž ์ •๋ณด ์—…๋ฐ์ดํŠธ\n(๋‹น์ฒจ ์—ฌ๋ถ€๋ฅผ true๋กœ ์„ค์ •, ๋‹น์ฒจ ์ผ์‹œ ๊ธฐ๋ก) +activate PartDB +PartDB --> PartService: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ +deactivate PartDB + +PartService -> PartDB: ์ถ”์ฒจ ๋กœ๊ทธ ์ €์žฅ\n(์ด๋ฒคํŠธID, ์ถ”์ฒจ๋ฐฉ๋ฒ•, ๋‹น์ฒจ์ธ์›,\n์•Œ๊ณ ๋ฆฌ์ฆ˜, ์ถ”์ฒจ์ผ์‹œ) +activate PartDB +note right of PartDB + ์ถ”์ฒจ ๋กœ๊ทธ ์ €์žฅ: + - ์ถ”์ฒจ ์ผ์‹œ + - ์ถ”์ฒจ ๋ฐฉ๋ฒ• + - ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋ฒ„์ „ + - ๊ฐ€์‚ฐ์  ์ ์šฉ ์—ฌ๋ถ€ +end note +PartDB --> PartService: ๋กœ๊ทธ ์ €์žฅ ์™„๋ฃŒ +deactivate PartDB + +PartService --> Gateway: 200 OK\n{๋‹น์ฒจ์ž๋ชฉ๋ก, ์ถ”์ฒจ๋กœ๊ทธID} +deactivate PartService + +Gateway --> OwnerFE: 200 OK +deactivate Gateway + +OwnerFE -> Owner: ๋‹น์ฒจ์ž ๋ชฉ๋ก ํ™”๋ฉด ํ‘œ์‹œ\n- ๋‹น์ฒจ์ž ์ •๋ณด (์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์‘๋ชจ๋ฒˆํ˜ธ)\n- ์ถ”์ฒจ ์™„๋ฃŒ ๋ฉ”์‹œ์ง€ +deactivate OwnerFE + +@enduml diff --git a/design/backend/sequence/outer/์‚ฌ์šฉ์ž์ธ์ฆํ”Œ๋กœ์šฐ.puml b/design/backend/sequence/outer/์‚ฌ์šฉ์ž์ธ์ฆํ”Œ๋กœ์šฐ.puml new file mode 100644 index 0000000..e5d624b --- /dev/null +++ b/design/backend/sequence/outer/์‚ฌ์šฉ์ž์ธ์ฆํ”Œ๋กœ์šฐ.puml @@ -0,0 +1,177 @@ +@startuml ์‚ฌ์šฉ์ž์ธ์ฆํ”Œ๋กœ์šฐ +!theme mono + +title KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ์‚ฌ์šฉ์ž ์ธ์ฆ ํ”Œ๋กœ์šฐ (์™ธ๋ถ€ ์‹œํ€€์Šค) + +actor "์‚ฌ์šฉ์ž\n(์†Œ์ƒ๊ณต์ธ)" as User +participant "Frontend\n(Web/Mobile)" as Frontend +participant "API Gateway" as Gateway +participant "User Service" as UserService +database "User DB\n(PostgreSQL)" as UserDB + +== UFR-USER-010: ํšŒ์›๊ฐ€์ž… ํ”Œ๋กœ์šฐ == + +User -> Frontend: ํšŒ์›๊ฐ€์ž… ํ™”๋ฉด ์ ‘๊ทผ +activate Frontend + +User -> Frontend: ํšŒ์› ์ •๋ณด ์ž…๋ ฅ\n(์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ,\n๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ, ์‚ฌ์—…์ž๋ฒˆํ˜ธ) +Frontend -> Frontend: ํด๋ผ์ด์–ธํŠธ ์ธก ์œ ํšจ์„ฑ ๊ฒ€์ฆ\n(์ด๋ฉ”์ผ ํ˜•์‹, ๋น„๋ฐ€๋ฒˆํ˜ธ 8์ž ์ด์ƒ ๋“ฑ) + +Frontend -> Gateway: POST /api/users/register\n(ํšŒ์› ์ •๋ณด) +activate Gateway + +Gateway -> Gateway: Request ๊ฒ€์ฆ\n(ํ•„์ˆ˜ ํ•„๋“œ, ๋ฐ์ดํ„ฐ ํƒ€์ž…) + +Gateway -> UserService: POST /api/users/register\n(ํšŒ์› ์ •๋ณด) +activate UserService + +UserService -> UserService: ์„œ๋ฒ„ ์ธก ์œ ํšจ์„ฑ ๊ฒ€์ฆ\n(์ด๋ฆ„ 2์ž ์ด์ƒ, ์ „ํ™”๋ฒˆํ˜ธ ํ˜•์‹ ๋“ฑ) + +UserService -> UserDB: ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ\n(์ค‘๋ณต ๊ฐ€์ž… ํ™•์ธ) +activate UserDB +UserDB --> UserService: ๊ธฐ์กด ์‚ฌ์šฉ์ž ํ™•์ธ ๊ฒฐ๊ณผ +deactivate UserDB + +alt ์ด๋ฉ”์ผ ์ค‘๋ณต ์กด์žฌ + UserService --> Gateway: 400 Bad Request\n(์ด๋ฏธ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ) + Gateway --> Frontend: 400 Bad Request + Frontend --> User: "์ด๋ฏธ ๊ฐ€์ž…๋œ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค" +else ์ด๋ฉ”์ผ ์‹ ๊ทœ + + UserService -> UserDB: ์ „ํ™”๋ฒˆํ˜ธ๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ\n(์ค‘๋ณต ๊ฐ€์ž… ํ™•์ธ) + activate UserDB + UserDB --> UserService: ๊ธฐ์กด ์‚ฌ์šฉ์ž ํ™•์ธ ๊ฒฐ๊ณผ + deactivate UserDB + + alt ์ „ํ™”๋ฒˆํ˜ธ ์ค‘๋ณต ์กด์žฌ + UserService --> Gateway: 400 Bad Request\n(์ด๋ฏธ ๋“ฑ๋ก๋œ ์ „ํ™”๋ฒˆํ˜ธ) + Gateway --> Frontend: 400 Bad Request + Frontend --> User: "์ด๋ฏธ ๊ฐ€์ž…๋œ ์ „ํ™”๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค" + else ์‹ ๊ทœ ์‚ฌ์šฉ์ž + + UserService -> UserService: ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹ฑ\n(bcrypt, Cost Factor 10) + + UserService -> UserService: ์‚ฌ์—…์ž๋ฒˆํ˜ธ ์•”ํ˜ธํ™”\n(AES-256) + + UserService -> UserDB: ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ + activate UserDB + + UserService -> UserDB: ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ\n(์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ,\n๋น„๋ฐ€๋ฒˆํ˜ธํ•ด์‹œ, ์ƒ์„ฑ์ผ์‹œ) + UserDB --> UserService: user_id ๋ฐ˜ํ™˜ + + UserService -> UserDB: ๋งค์žฅ ์ •๋ณด ์ €์žฅ\n(์‚ฌ์šฉ์žID, ๋งค์žฅ๋ช…, ์—…์ข…,\n์ฃผ์†Œ, ์•”ํ˜ธํ™”๋œ์‚ฌ์—…์ž๋ฒˆํ˜ธ,\n์˜์—…์‹œ๊ฐ„) + UserDB --> UserService: store_id ๋ฐ˜ํ™˜ + + UserService -> UserDB: ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ + deactivate UserDB + + UserService -> UserService: JWT ํ† ํฐ ์ƒ์„ฑ\n(user_id, role=OWNER,\nexp=7์ผ) + + UserService --> Gateway: 201 Created\n(JWT ํ† ํฐ, ์‚ฌ์šฉ์ž ์ •๋ณด) + deactivate UserService + + Gateway --> Frontend: 201 Created\n(JWT ํ† ํฐ, ์‚ฌ์šฉ์ž ์ •๋ณด) + deactivate Gateway + + Frontend -> Frontend: JWT ํ† ํฐ ์ €์žฅ\n(LocalStorage ๋˜๋Š” Cookie) + + Frontend --> User: "ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + + Frontend -> Gateway: ๋Œ€์‹œ๋ณด๋“œ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + deactivate Frontend + end +end + +== UFR-USER-020: ๋กœ๊ทธ์ธ ํ”Œ๋กœ์šฐ == + +User -> Frontend: ๋กœ๊ทธ์ธ ํ™”๋ฉด ์ ‘๊ทผ +activate Frontend + +User -> Frontend: ์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ + +Frontend -> Frontend: ํด๋ผ์ด์–ธํŠธ ์ธก ์œ ํšจ์„ฑ ๊ฒ€์ฆ\n(ํ•„์ˆ˜ ํ•„๋“œ ํ™•์ธ, ์ด๋ฉ”์ผ ํ˜•์‹) + +Frontend -> Gateway: POST /api/users/login\n(์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ) +activate Gateway + +Gateway -> Gateway: Request ๊ฒ€์ฆ + +Gateway -> UserService: POST /api/users/login\n(์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ) +activate UserService + +UserService -> UserDB: ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ\n(๋กœ๊ทธ์ธ ์ธ์ฆ์šฉ) +activate UserDB +UserDB --> UserService: ์‚ฌ์šฉ์ž ์ •๋ณด\n(user_id, password_hash, role) +deactivate UserDB + +alt ์‚ฌ์šฉ์ž ์—†์Œ + UserService --> Gateway: 401 Unauthorized\n(์ธ์ฆ ์‹คํŒจ) + Gateway --> Frontend: 401 Unauthorized + Frontend --> User: "์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ\nํ™•์ธํ•ด์ฃผ์„ธ์š”" +else ์‚ฌ์šฉ์ž ์กด์žฌ + + UserService -> UserService: ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ\n(bcrypt compare) + + alt ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ + UserService --> Gateway: 401 Unauthorized\n(์ธ์ฆ ์‹คํŒจ) + Gateway --> Frontend: 401 Unauthorized + Frontend --> User: "์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ\nํ™•์ธํ•ด์ฃผ์„ธ์š”" + else ๋น„๋ฐ€๋ฒˆํ˜ธ ์ผ์น˜ + + UserService -> UserService: JWT ํ† ํฐ ์ƒ์„ฑ\n(user_id, role=OWNER,\nexp=7์ผ) + + UserService -> UserDB: ์ตœ์ข… ๋กœ๊ทธ์ธ ์‹œ๊ฐ ์—…๋ฐ์ดํŠธ\n(ํ˜„์žฌ ์‹œ๊ฐ์œผ๋กœ ๊ฐฑ์‹ ) + activate UserDB + UserDB --> UserService: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ + deactivate UserDB + + UserService --> Gateway: 200 OK\n(JWT ํ† ํฐ, ์‚ฌ์šฉ์ž ์ •๋ณด) + deactivate UserService + + Gateway --> Frontend: 200 OK\n(JWT ํ† ํฐ, ์‚ฌ์šฉ์ž ์ •๋ณด) + deactivate Gateway + + Frontend -> Frontend: JWT ํ† ํฐ ์ €์žฅ\n(LocalStorage ๋˜๋Š” Cookie) + + Frontend --> User: ๋กœ๊ทธ์ธ ์„ฑ๊ณต + + Frontend -> Gateway: ๋Œ€์‹œ๋ณด๋“œ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + deactivate Frontend + end +end + +== UFR-USER-040: ๋กœ๊ทธ์•„์›ƒ ํ”Œ๋กœ์šฐ == + +User -> Frontend: ํ”„๋กœํ•„ ํƒญ ์ ‘๊ทผ +activate Frontend + +User -> Frontend: "๋กœ๊ทธ์•„์›ƒ" ๋ฒ„ํŠผ ํด๋ฆญ + +Frontend -> Frontend: ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ\n"๋กœ๊ทธ์•„์›ƒ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + +User -> Frontend: "ํ™•์ธ" ํด๋ฆญ + +Frontend -> Gateway: POST /api/users/logout\nAuthorization: Bearer {JWT} +activate Gateway + +Gateway -> Gateway: JWT ํ† ํฐ ๊ฒ€์ฆ + +Gateway -> UserService: POST /api/users/logout\n(JWT ํ† ํฐ) +activate UserService + +UserService -> UserService: JWT ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€\n(๋งŒ๋ฃŒ ์‹œ๊นŒ์ง€ ์œ ํšจ) + +UserService --> Gateway: 200 OK\n(๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต) +deactivate UserService + +Gateway --> Frontend: 200 OK +deactivate Gateway + +Frontend -> Frontend: JWT ํ† ํฐ ์‚ญ์ œ\n(LocalStorage ๋˜๋Š” Cookie) + +Frontend --> User: "์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + +Frontend -> Gateway: ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ์ด๋™ +deactivate Frontend + +@enduml diff --git a/design/backend/sequence/outer/์„ฑ๊ณผ๋ถ„์„ํ”Œ๋กœ์šฐ.puml b/design/backend/sequence/outer/์„ฑ๊ณผ๋ถ„์„ํ”Œ๋กœ์šฐ.puml new file mode 100644 index 0000000..8db66c6 --- /dev/null +++ b/design/backend/sequence/outer/์„ฑ๊ณผ๋ถ„์„ํ”Œ๋กœ์šฐ.puml @@ -0,0 +1,198 @@ +@startuml ์„ฑ๊ณผ๋ถ„์„ํ”Œ๋กœ์šฐ_์™ธ๋ถ€์‹œํ€€์Šค +!theme mono + +title ์„ฑ๊ณผ ๋ถ„์„ ํ”Œ๋กœ์šฐ - ์™ธ๋ถ€ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ\n(UFR-ANAL-010: ์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ) + +actor "์†Œ์ƒ๊ณต์ธ" as User +participant "Frontend" as FE +participant "API Gateway" as GW +participant "Analytics Service" as Analytics +participant "Redis Cache\n(TTL 1์‹œ๊ฐ„)" as Redis +participant "Analytics DB" as AnalyticsDB +participant "Kafka\n(Event Topics)" as Kafka + +note over AnalyticsDB + **๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋กœ ์ˆ˜์ง‘๋œ ๋ฐ์ดํ„ฐ** + - ์™ธ๋ถ€ ์ฑ„๋„ ํ†ต๊ณ„๋Š” ๋ฐฐ์น˜ ์ž‘์—…์œผ๋กœ + ์ฃผ๊ธฐ์ ์œผ๋กœ ์ˆ˜์ง‘ํ•˜์—ฌ DB์— ์ €์žฅ + - ๋ชฉ์—… ๋ฐ์ดํ„ฐ๋กœ ์‹œ์ž‘, ์ ์ง„์ ์œผ๋กœ ์‹ค์ œ API ์—ฐ๋™ +end note + +== 1. ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ - Cache HIT ์‹œ๋‚˜๋ฆฌ์˜ค == + +User -> FE: ์„ฑ๊ณผ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ์ ‘๊ทผ\n(Bottom Nav "๋ถ„์„" ํƒญ ํด๋ฆญ) +activate FE + +FE -> GW: GET /api/events/{id}/analytics\n+ Authorization: Bearer {token} +activate GW + +GW -> GW: JWT ํ† ํฐ ๊ฒ€์ฆ +GW -> Analytics: GET /api/events/{id}/analytics +activate Analytics + +Analytics -> Redis: ๋Œ€์‹œ๋ณด๋“œ ์บ์‹œ ์กฐํšŒ\n(์บ์‹œํ‚ค: analytics:dashboard:{eventId}) +activate Redis +Redis --> Analytics: **Cache HIT**\n์บ์‹œ๋œ ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ +deactivate Redis + +note right of Analytics + **Cache-Aside ํŒจํ„ด** + - TTL: 1์‹œ๊ฐ„ + - ์˜ˆ์ƒ ํฌ๊ธฐ: 5KB + - ํžˆํŠธ์œจ ๋ชฉํ‘œ: 95% + - ์‘๋‹ต ์‹œ๊ฐ„: 0.5์ดˆ +end note + +Analytics --> GW: 200 OK\n๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ (JSON) +deactivate Analytics + +GW --> FE: 200 OK\n๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ +deactivate GW + +FE -> FE: ๋Œ€์‹œ๋ณด๋“œ ๋ Œ๋”๋ง\n- 4๊ฐœ ์š”์•ฝ ์นด๋“œ\n- ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ์ฐจํŠธ\n- ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด +FE --> User: ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ํ‘œ์‹œ +deactivate FE + +== 2. ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ - Cache MISS ์‹œ๋‚˜๋ฆฌ์˜ค == + +User -> FE: ๋Œ€์‹œ๋ณด๋“œ ์ƒˆ๋กœ๊ณ ์นจ\n๋˜๋Š” ์ฒซ ์กฐํšŒ +activate FE + +FE -> GW: GET /api/events/{id}/analytics +activate GW + +GW -> Analytics: GET /api/events/{id}/analytics +activate Analytics + +Analytics -> Redis: ๋Œ€์‹œ๋ณด๋“œ ์บ์‹œ ์กฐํšŒ\n(์บ์‹œํ‚ค: analytics:dashboard:{eventId}) +activate Redis +Redis --> Analytics: **Cache MISS**\nnull ๋ฐ˜ํ™˜ +deactivate Redis + +note right of Analytics + **๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ์ž‘์—… ์‹œ์ž‘** + - Analytics DB ์กฐํšŒ + - ์™ธ๋ถ€ ์ฑ„๋„ API ๋ณ‘๋ ฌ ํ˜ธ์ถœ + - Circuit Breaker ์ ์šฉ +end note + +||| +== 2.1. Analytics DB ์กฐํšŒ (๋กœ์ปฌ ๋ฐ์ดํ„ฐ) == + +Analytics -> AnalyticsDB: ์ด๋ฒคํŠธ ํ†ต๊ณ„ ์กฐํšŒ\n(์ด๋ฒคํŠธID๋กœ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ) +activate AnalyticsDB +AnalyticsDB --> Analytics: ์ด๋ฒคํŠธ ํ†ต๊ณ„\n- ์ด ์ฐธ์—ฌ์ž ์ˆ˜\n- ์˜ˆ์ƒ ROI\n- ๋งค์ถœ ์ฆ๊ฐ€์œจ +deactivate AnalyticsDB + +||| +== 2.2. ๋ฐฐ์น˜ ์ˆ˜์ง‘๋œ ์ฑ„๋„ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ == + +Analytics -> AnalyticsDB: ์ฑ„๋„๋ณ„ ํ†ต๊ณ„ ์กฐํšŒ\n(๋ฐฐ์น˜๋กœ ์ˆ˜์ง‘๋œ ์ฑ„๋„ ๋ฐ์ดํ„ฐ ์กฐํšŒ) +activate AnalyticsDB + +note right of Analytics + **๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋ฐฉ์‹** + - ์™ธ๋ถ€ API๋Š” ๋ณ„๋„ ๋ฐฐ์น˜ ์ž‘์—…์œผ๋กœ ์ฃผ๊ธฐ์  ์ˆ˜์ง‘ + - ์ˆ˜์ง‘๋œ ๋ฐ์ดํ„ฐ๋Š” DB์— ์ €์žฅ + - ๋Œ€์‹œ๋ณด๋“œ์—์„œ๋Š” DB ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒ + - ์‘๋‹ต ์‹œ๊ฐ„ ๋‹จ์ถ• ๋ฐ ์™ธ๋ถ€ API ์˜์กด์„ฑ ์ œ๊ฑฐ +end note + +AnalyticsDB --> Analytics: ์ฑ„๋„๋ณ„ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ\n- ์šฐ๋ฆฌ๋™๋„คTV: ๋…ธ์ถœ 5,000, ์กฐํšŒ 1,200\n- ์ง€๋‹ˆTV: ๋…ธ์ถœ 10,000, ํด๋ฆญ 500\n- Instagram: ์ข‹์•„์š” 300, ๋Œ“๊ธ€ 50\n- Naver: ์กฐํšŒ 2,000\n- Kakao: ๊ณต์œ  100 +deactivate AnalyticsDB + +note right of Analytics + **๋ชฉ์—… ๋ฐ์ดํ„ฐ ํ™œ์šฉ** + - ์ดˆ๊ธฐ์—๋Š” ๋ชฉ์—… ๋ฐ์ดํ„ฐ๋กœ ์‹œ์ž‘ + - ์ ์ง„์ ์œผ๋กœ ์‹ค์ œ ๋ฐฐ์น˜ ์ž‘์—… ๊ตฌํ˜„ + - ๋ฐฐ์น˜ ์ฃผ๊ธฐ: 5๋ถ„๋งˆ๋‹ค ์ˆ˜์ง‘ +end note + +||| +== 2.3. ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ๋ฐ ROI ๊ณ„์‚ฐ == + +Analytics -> Analytics: ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ ๋ฐ ๊ณ„์‚ฐ\n- ์ด ๋…ธ์ถœ ์ˆ˜ = ์™ธ๋ถ€ ์ฑ„๋„ ๋…ธ์ถœ ํ•ฉ๊ณ„\n- ์ด ์ฐธ์—ฌ์ž ์ˆ˜ = Analytics DB\n- ROI ๊ณ„์‚ฐ = (์ˆ˜์ต - ๋น„์šฉ) / ๋น„์šฉ ร— 100\n- ์ฑ„๋„๋ณ„ ์ „ํ™˜์œจ ๊ณ„์‚ฐ + +note right of Analytics + **ROI ๊ณ„์‚ฐ ๋กœ์ง** + ์ด ๋น„์šฉ = ๊ฒฝํ’ˆ ๋น„์šฉ + ํ”Œ๋žซํผ ๋น„์šฉ + ์˜ˆ์ƒ ์ˆ˜์ต = ๋งค์ถœ ์ฆ๊ฐ€์•ก + ์‹ ๊ทœ ๊ณ ๊ฐ LTV + ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  = (์ˆ˜์ต - ๋น„์šฉ) / ๋น„์šฉ ร— 100 +end note + +||| +== 2.4. Redis ์บ์‹ฑ ๋ฐ ์‘๋‹ต == + +Analytics -> Redis: ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ์บ์‹œ ์ €์žฅ\n(์บ์‹œํ‚ค: analytics:dashboard:{eventId},\n๊ฐ’: ํ†ตํ•ฉ ๋ฐ์ดํ„ฐ, TTL: 1์‹œ๊ฐ„) +activate Redis +Redis --> Analytics: OK +deactivate Redis + +Analytics --> GW: 200 OK\n๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ (JSON)\n{\n ์ด์ฐธ์—ฌ์ž: 1,234,\n ์ด๋…ธ์ถœ: 17,200,\n ROI: 250%,\n ์ฑ„๋„๋ณ„์„ฑ๊ณผ: [...]\n} +deactivate Analytics + +GW --> FE: 200 OK\n๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ +deactivate GW + +FE -> FE: ๋Œ€์‹œ๋ณด๋“œ ๋ Œ๋”๋ง\n- 4๊ฐœ ์š”์•ฝ ์นด๋“œ ํ‘œ์‹œ\n- ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ์ฐจํŠธ\n- ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด\n- ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ถ„์„ +FE --> User: ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ํ‘œ์‹œ\n(์‘๋‹ต ์‹œ๊ฐ„: 3์ดˆ) +deactivate FE + +||| +== 3. ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (Background Event ๊ตฌ๋…) == + +note over Analytics, Kafka + **Analytics Service๋Š” ํ•ญ์ƒ Background์—์„œ + Kafka Event Topics๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ + ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ†ต๊ณ„๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค** +end note + +Kafka -> Analytics: **EventCreated** ์ด๋ฒคํŠธ\n{eventId, storeId, title, objective} +activate Analytics +Analytics -> AnalyticsDB: ์ด๋ฒคํŠธ ํ†ต๊ณ„ ์ดˆ๊ธฐํ™”\n(์ด๋ฒคํŠธ ๊ธฐ๋ณธ ์ •๋ณด ์ €์žฅ) +activate AnalyticsDB +AnalyticsDB --> Analytics: OK +deactivate AnalyticsDB +Analytics -> Redis: ์บ์‹œ ๋ฌดํšจํ™”\n(์บ์‹œํ‚ค ์‚ญ์ œ: analytics:dashboard:{eventId}) +activate Redis +Redis --> Analytics: OK +deactivate Redis +deactivate Analytics + +...์ฐธ์—ฌ์ž ๋“ฑ๋ก ์‹œ... + +Kafka -> Analytics: **ParticipantRegistered** ์ด๋ฒคํŠธ\n{participantId, eventId, phoneNumber} +activate Analytics +Analytics -> AnalyticsDB: ์ฐธ์—ฌ์ž ์ˆ˜ ์—…๋ฐ์ดํŠธ\n(์ฐธ์—ฌ์ž ์ˆ˜ 1 ์ฆ๊ฐ€) +activate AnalyticsDB +AnalyticsDB --> Analytics: OK +deactivate AnalyticsDB +Analytics -> Redis: ์บ์‹œ ๋ฌดํšจํ™”\n(์บ์‹œํ‚ค ์‚ญ์ œ: analytics:dashboard:{eventId}) +activate Redis +Redis --> Analytics: OK +deactivate Redis +deactivate Analytics + +...๋ฐฐํฌ ์™„๋ฃŒ ์‹œ... + +Kafka -> Analytics: **DistributionCompleted** ์ด๋ฒคํŠธ\n{eventId, distributedChannels, completedAt} +activate Analytics +Analytics -> AnalyticsDB: ์ฑ„๋„ ํ†ต๊ณ„ ์ €์žฅ\n(๋ฐฐํฌ ์™„๋ฃŒ๋œ ์ฑ„๋„ ์ •๋ณด ์ €์žฅ) +activate AnalyticsDB +AnalyticsDB --> Analytics: OK +deactivate AnalyticsDB +Analytics -> Redis: ์บ์‹œ ๋ฌดํšจํ™”\n(์บ์‹œํ‚ค ์‚ญ์ œ: analytics:dashboard:{eventId}) +activate Redis +Redis --> Analytics: OK +deactivate Redis +deactivate Analytics + +note right of Analytics + **์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ๋ฉ”์ปค๋‹ˆ์ฆ˜** + - EventCreated: ์ด๋ฒคํŠธ ๊ธฐ๋ณธ ์ •๋ณด ์ดˆ๊ธฐํ™” + - ParticipantRegistered: ์ฐธ์—ฌ์ž ์ˆ˜ ์‹ค์‹œ๊ฐ„ ์ฆ๊ฐ€ + - DistributionCompleted: ๋ฐฐํฌ ์ฑ„๋„ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ + - ์บ์‹œ ๋ฌดํšจํ™”: ๋‹ค์Œ ์กฐํšŒ ์‹œ ์ตœ์‹  ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  +end note + +@enduml diff --git a/design/backend/sequence/outer/์ด๋ฒคํŠธ์ƒ์„ฑํ”Œ๋กœ์šฐ.puml b/design/backend/sequence/outer/์ด๋ฒคํŠธ์ƒ์„ฑํ”Œ๋กœ์šฐ.puml new file mode 100644 index 0000000..a1933c9 --- /dev/null +++ b/design/backend/sequence/outer/์ด๋ฒคํŠธ์ƒ์„ฑํ”Œ๋กœ์šฐ.puml @@ -0,0 +1,250 @@ +@startuml ์ด๋ฒคํŠธ์ƒ์„ฑํ”Œ๋กœ์šฐ +!theme mono + +title ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ - ์™ธ๋ถ€ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ + +actor "์†Œ์ƒ๊ณต์ธ" as User +participant "Frontend" as FE +participant "API Gateway" as Gateway +participant "Event Service" as Event +participant "User Service" as UserSvc +participant "AI Service" as AI +participant "Content Service" as Content +participant "Distribution Service" as Dist +participant "Kafka" as Kafka +database "Event DB" as EventDB +database "User DB" as UserDB +database "Redis" as Redis +participant "์™ธ๋ถ€ AI API" as AIApi +participant "์ด๋ฏธ์ง€ ์ƒ์„ฑ API" as ImageApi +participant "๋ฐฐํฌ ์ฑ„๋„ APIs" as ChannelApis + +== 1. ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ (UFR-EVENT-020) == +User -> FE: ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ +FE -> Gateway: GET /api/users/{userId}/store\nํšŒ์› ๋ฐ ๋งค์žฅ์ •๋ณด ์กฐํšŒ +activate Gateway +Gateway -> UserSvc: GET /api/users/{userId}/store\nํšŒ์› ๋ฐ ๋งค์žฅ์ •๋ณด ์กฐํšŒ +activate UserSvc +UserSvc -> UserDB: ์‚ฌ์šฉ์ž ๋ฐ ๋งค์žฅ ์ •๋ณด ์กฐํšŒ +activate UserDB +UserDB --> UserSvc: ์‚ฌ์šฉ์ž, ๋งค์žฅ ์ •๋ณด ๋ฐ˜ํ™˜ +deactivate UserDB +UserSvc --> Gateway: 200 OK\n{userId, storeName, industry, address} +deactivate UserSvc +Gateway --> FE: 200 OK\n{userId, storeName, industry, address} +deactivate Gateway +FE -> Gateway: POST /events/purposes\n{๋ชฉ์ , userId, storeName, industry, address} +Gateway -> Event: ์ด๋ฒคํŠธ ๋ชฉ์  ์ €์žฅ ์š”์ฒญ +Event -> Redis: ์ด๋ฒคํŠธ ๋ชฉ์  ์ •๋ณด ์ €์žฅ\nKey: draft:event:{eventDraftId}\n(๋ชฉ์ , ๋งค์žฅ์ •๋ณด ์ €์žฅ)\nTTL: 24์‹œ๊ฐ„ +activate Redis +Redis --> Event: ์ €์žฅ ์™„๋ฃŒ +deactivate Redis +Event --> Gateway: ์ €์žฅ ์™„๋ฃŒ\n{eventDraftId} +Gateway --> FE: 200 OK +FE --> User: AI ์ถ”์ฒœ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + +== 2. AI ์ด๋ฒคํŠธ ์ถ”์ฒœ - ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ (UFR-EVENT-030) == +User -> FE: AI ์ถ”์ฒœ ์š”์ฒญ +FE -> Gateway: POST /api/events/{eventDraftId}/ai-recommendations\n{๋ชฉ์ , ์—…์ข…, ์ง€์—ญ} +Gateway -> Event: AI ์ถ”์ฒœ ์š”์ฒญ ์ „๋‹ฌ +Event -> Kafka: Publish to ai-job-topic\n{jobId, eventDraftId, ๋ชฉ์ , ์—…์ข…, ์ง€์—ญ} +Event --> Gateway: Job ์ƒ์„ฑ ์™„๋ฃŒ\n{jobId, status: PENDING} +Gateway --> FE: 202 Accepted\n{jobId} +FE --> User: "AI๊ฐ€ ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค..." (๋กœ๋”ฉ) + +note over AI: Kafka Consumer\nai ์ด๋ฒคํŠธ ์ƒ์„ฑ topic ๊ตฌ๋… +Kafka --> AI: Consume Job Message\n{jobId, eventDraftId, ...} + +AI -> AIApi: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ ์š”์ฒญ\n{๋ชฉ์ , ์—…์ข…, ์ง€์—ญ, ๋งค์žฅ์ •๋ณด}\n[Circuit Breaker, Timeout: 5๋ถ„] +AIApi --> AI: 3๊ฐ€์ง€ ์ถ”์ฒœ์•ˆ + ํŠธ๋ Œ๋“œ ์š”์•ฝ\n(์˜ˆ: "์—ฌ๋ฆ„์ฒ  ์‹œ์›ํ•œ ์Œ๋ฃŒ ์„ ํ˜ธ๋„ ์ฆ๊ฐ€") +AI -> Redis: AI ์ถ”์ฒœ ๊ฒฐ๊ณผ ์ €์žฅ\nKey: ai:event:{eventDraftId}\n(3๊ฐ€์ง€ ์ถ”์ฒœ์•ˆ, ํŠธ๋ Œ๋“œ ์š”์•ฝ)\nTTL: 24์‹œ๊ฐ„ +Redis --> AI: ์ €์žฅ ์™„๋ฃŒ +AI -> Redis: Job ์ƒํƒœ ์—…๋ฐ์ดํŠธ\n(์ƒํƒœ๋ฅผ COMPLETED๋กœ ๋ณ€๊ฒฝ) +note over AI, Redis: AI ์ถ”์ฒœ ์ •๋ณด๋Š” Redis์— ์ €์žฅ\n- Content Service๊ฐ€ ์ฝ๊ธฐ ์œ„ํ•จ\n- ์ตœ์ข… ์Šน์ธ ์‹œ Event DB์— ์˜๊ตฌ ์ €์žฅ + +group Polling์œผ๋กœ ์ƒํƒœ ํ™•์ธ + loop ์ƒํƒœ ํ™•์ธ (์ตœ๋Œ€ 30์ดˆ) + FE -> Gateway: GET /jobs/{jobId}/status + Gateway -> Event: Job ์ƒํƒœ ์กฐํšŒ + Event -> Redis: Job ์ƒํƒœ ์กฐํšŒ\n(jobId๋กœ ์ƒํƒœ ๋ฐ ๊ฒฐ๊ณผ ์กฐํšŒ) + Redis --> Event: {status, result} + + alt Job ์™„๋ฃŒ + Event --> Gateway: 200 OK\n{status: COMPLETED, recommendations, trendSummary} + Gateway --> FE: ์ถ”์ฒœ ๊ฒฐ๊ณผ ๋ฐ ํŠธ๋ Œ๋“œ ์š”์•ฝ ๋ฐ˜ํ™˜ + FE --> User: ํŠธ๋ Œ๋“œ ์š”์•ฝ ํ‘œ์‹œ\n3๊ฐ€์ง€ ์ถ”์ฒœ์•ˆ ํ‘œ์‹œ\n(์ œ๋ชฉ/๊ฒฝํ’ˆ ์ˆ˜์ • ๊ฐ€๋Šฅ) + else Job ์ง„ํ–‰์ค‘ + Event --> Gateway: 200 OK\n{status: PENDING/PROCESSING} + Gateway --> FE: ์ง„ํ–‰์ค‘ ์ƒํƒœ + note over FE: 2์ดˆ ํ›„ ์žฌ์š”์ฒญ + end + end +end + +User -> FE: ์ถ”์ฒœ์•ˆ ์„ ํƒ\n(์ œ๋ชฉ/๊ฒฝํ’ˆ ์ปค์Šคํ…€) +FE -> Gateway: PUT /events/drafts/{eventDraftId}\n{์„ ํƒํ•œ ์ถ”์ฒœ์•ˆ, ์ปค์Šคํ…€ ์ •๋ณด} +Gateway -> Event: ์„ ํƒ ์ €์žฅ +Event -> Redis: ์„ ํƒํ•œ ์ถ”์ฒœ์•ˆ ์ €์žฅ\nKey: draft:event:{eventDraftId}\n(์ด๋ฒคํŠธ ์ดˆ์•ˆ ์—…๋ฐ์ดํŠธ)\nTTL: 24์‹œ๊ฐ„ +activate Redis +Redis --> Event: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ +deactivate Redis +Event --> Gateway: 200 OK +Gateway --> FE: ์ €์žฅ ์™„๋ฃŒ +FE --> User: ์ฝ˜ํ…์ธ  ์ƒ์„ฑ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + +== 3. SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ - ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ (UFR-CONT-010) == +User -> FE: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ +FE -> Gateway: POST /api/content/images/{eventDraftId}/generate +Gateway -> Content: ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ +Content -> Content: Job ์ƒ์„ฑ\n{jobId, eventDraftId, status: PENDING} +Content --> Gateway: Job ์ƒ์„ฑ ์™„๋ฃŒ\n{jobId, status: PENDING} +Gateway --> FE: 202 Accepted\n{jobId} +FE --> User: "์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘..." (๋กœ๋”ฉ) + +note over Content: ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์›Œ์ปค\nRedis ํด๋ง ๋˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ + +Content -> Redis: AI ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ\nKey: ai:event:{eventDraftId} +activate Redis +Redis --> Content: AI ์ถ”์ฒœ ๊ฒฐ๊ณผ\n{์„ ํƒ๋œ ์ถ”์ฒœ์•ˆ, ์ด๋ฒคํŠธ ์ •๋ณด} +deactivate Redis + +note over Content: inner sequence ์ฐธ์กฐ:\ncontent-์ด๋ฏธ์ง€์ƒ์„ฑ.puml + +par ์‹ฌํ”Œ ์Šคํƒ€์ผ + Content -> ImageApi: ์‹ฌํ”Œ ์Šคํƒ€์ผ ์ƒ์„ฑ ์š”์ฒญ\n[Circuit Breaker, Timeout: 5๋ถ„] + ImageApi --> Content: ์‹ฌํ”Œ ์ด๋ฏธ์ง€ URL +else ํ™”๋ คํ•œ ์Šคํƒ€์ผ + Content -> ImageApi: ํ™”๋ คํ•œ ์Šคํƒ€์ผ ์ƒ์„ฑ ์š”์ฒญ\n[Circuit Breaker, Timeout: 5๋ถ„] + ImageApi --> Content: ํ™”๋ คํ•œ ์ด๋ฏธ์ง€ URL +else ํŠธ๋ Œ๋”” ์Šคํƒ€์ผ + Content -> ImageApi: ํŠธ๋ Œ๋”” ์Šคํƒ€์ผ ์ƒ์„ฑ ์š”์ฒญ\n[Circuit Breaker, Timeout: 5๋ถ„] + ImageApi --> Content: ํŠธ๋ Œ๋”” ์ด๋ฏธ์ง€ URL +end + +Content -> Redis: ์ด๋ฏธ์ง€ URL ์ €์žฅ\nKey: content:image:{eventDraftId}\n{์‹ฌํ”Œ, ํ™”๋ ค, ํŠธ๋ Œ๋”” URL}\nTTL: 7์ผ +activate Redis +Redis --> Content: ์ €์žฅ ์™„๋ฃŒ +deactivate Redis +Content -> Redis: Job ์ƒํƒœ ์—…๋ฐ์ดํŠธ\n(์ƒํƒœ๋ฅผ COMPLETED๋กœ ๋ณ€๊ฒฝ) +note over Content, Redis: ์ด๋ฏธ์ง€ URL์€ Redis์— ์ €์žฅ\n- ์ตœ์ข… ์Šน์ธ ์‹œ Event DB์— ์˜๊ตฌ ์ €์žฅ + +group Polling์œผ๋กœ ์ƒํƒœ ํ™•์ธ + loop ์ƒํƒœ ํ™•์ธ (์ตœ๋Œ€ 30์ดˆ) + FE -> Gateway: GET /api/content/jobs/{jobId}/status + Gateway -> Content: Job ์ƒํƒœ ์กฐํšŒ + Content -> Redis: Job ์ƒํƒœ ์กฐํšŒ\n(jobId๋กœ ์ƒํƒœ ๋ฐ ์ด๋ฏธ์ง€ URL ์กฐํšŒ) + Redis --> Content: {status, imageUrls} + + alt Job ์™„๋ฃŒ + Content --> Gateway: 200 OK\n{status: COMPLETED, imageUrls} + Gateway --> FE: ์ด๋ฏธ์ง€ URL ๋ฐ˜ํ™˜ + FE --> User: 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์นด๋“œ ํ‘œ์‹œ + else Job ์ง„ํ–‰์ค‘ + Content --> Gateway: 200 OK\n{status: PENDING/PROCESSING} + Gateway --> FE: ์ง„ํ–‰์ค‘ ์ƒํƒœ + note over FE: 2์ดˆ ํ›„ ์žฌ์š”์ฒญ + end + end +end + +User -> FE: ์Šคํƒ€์ผ ์„ ํƒ ๋ฐ ํŽธ์ง‘ +FE -> Gateway: PUT /events/drafts/{eventDraftId}/content\n{์„ ํƒํ•œ ์ด๋ฏธ์ง€, ํŽธ์ง‘๋‚ด์šฉ} +Gateway -> Event: ์ฝ˜ํ…์ธ  ์„ ํƒ ์ €์žฅ +Event -> Redis: ์„ ํƒํ•œ ์ฝ˜ํ…์ธ  ์ €์žฅ\nKey: draft:event:{eventDraftId}\n(์ด๋ฒคํŠธ ์ดˆ์•ˆ ์—…๋ฐ์ดํŠธ)\nTTL: 24์‹œ๊ฐ„ +activate Redis +Redis --> Event: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ +deactivate Redis +Event --> Gateway: 200 OK +Gateway --> FE: ์ €์žฅ ์™„๋ฃŒ +FE --> User: ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + +== 4. ์ตœ์ข… ์Šน์ธ ๋ฐ ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ - ๋™๊ธฐ ์ฒ˜๋ฆฌ (UFR-EVENT-050) == +User -> FE: ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ\n์ตœ์ข… ์Šน์ธ ์š”์ฒญ +FE -> Gateway: POST /api/events/{eventDraftId}/publish\n{์„ ํƒ ์ฑ„๋„ ๋ชฉ๋ก} +Gateway -> Event: ์ตœ์ข… ์Šน์ธ ๋ฐ ๋ฐฐํฌ ์ฒ˜๋ฆฌ + +note over Event: Redis ๋ฐ์ดํ„ฐ๋ฅผ Event DB์— ์˜๊ตฌ ์ €์žฅ + +Event -> Redis: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ์กฐํšŒ\nKey: draft:event:{eventDraftId} +activate Redis +Redis --> Event: ์ด๋ฒคํŠธ ์ดˆ์•ˆ ๋ฐ์ดํ„ฐ\n(๋ชฉ์ , ๋งค์žฅ์ •๋ณด, ์ถ”์ฒœ์•ˆ, ์ฝ˜ํ…์ธ ) +deactivate Redis + +Event -> Redis: AI ์ถ”์ฒœ ๊ฒฐ๊ณผ ์กฐํšŒ\nKey: ai:event:{eventDraftId} +activate Redis +Redis --> Event: AI ์ถ”์ฒœ ๊ฒฐ๊ณผ +deactivate Redis + +Event -> Redis: ์ด๋ฏธ์ง€ URL ์กฐํšŒ\nKey: content:image:{eventDraftId} +activate Redis +Redis --> Event: ์ด๋ฏธ์ง€ URL ๋ชฉ๋ก +deactivate Redis + +Event -> EventDB: ์ด๋ฒคํŠธ ์ •๋ณด ์˜๊ตฌ ์ €์žฅ\n(๋ชฉ์ , ๋งค์žฅ์ •๋ณด, AI ์ถ”์ฒœ, ์ด๋ฏธ์ง€ URL, ๋ฐฐํฌ ์ฑ„๋„ ํฌํ•จ) +EventDB --> Event: ์ €์žฅ ์™„๋ฃŒ + +Event -> EventDB: ์ด๋ฒคํŠธ ์ƒํƒœ ๋ณ€๊ฒฝ\n(DRAFT โ†’ APPROVED๋กœ ์—…๋ฐ์ดํŠธ) +EventDB --> Event: ์ƒํƒœ ๋ณ€๊ฒฝ ์™„๋ฃŒ +Event -> Kafka: Publish to event-topic\nEventCreated\n{eventId, ์ด๋ฒคํŠธ์ •๋ณด} + +note over Event: ๋™๊ธฐ ํ˜ธ์ถœ๋กœ ๋ฐฐํฌ ์ง„ํ–‰\ninner sequence ์ฐธ์กฐ:\ndistribution-๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ.puml +Event -> Dist: REST API - ๋ฐฐํฌ ์š”์ฒญ\nPOST /api/distribution/distribute\n{eventId, channels[], contentUrls} + +note over Dist: Sprint 2: Mock ์ฒ˜๋ฆฌ\n- ์™ธ๋ถ€ API ํ˜ธ์ถœ ์—†์Œ\n- ๋ชจ๋“  ๋ฐฐํฌ ์ฆ‰์‹œ ์„ฑ๊ณต ์ฒ˜๋ฆฌ\n- ๋ฐฐํฌ ๋กœ๊ทธ๋งŒ DB ๊ธฐ๋ก + +Dist -> EventDB: ๋ฐฐํฌ ์ด๋ ฅ ์ดˆ๊ธฐํ™”\n(์ด๋ฒคํŠธID, ์ƒํƒœ: PENDING) +EventDB --> Dist: ๋ฐฐํฌ ์ด๋ ฅ ID +Dist -> EventDB: ๋ฐฐํฌ ์ด๋ ฅ ์ƒํƒœ ์—…๋ฐ์ดํŠธ\n(์ƒํƒœ: IN_PROGRESS) + +note over Dist: ๋‹ค์ค‘ ์ฑ„๋„ Mock ๋ฐฐํฌ\n(๋‚ด๋ถ€ ์ฒ˜๋ฆฌ ์ƒ์„ธ๋Š” inner sequence ์ฐธ์กฐ) + +par ์šฐ๋ฆฌ๋™๋„คTV + alt ์šฐ๋ฆฌ๋™๋„คTV ์„ ํƒ + Dist -> Dist: Mock ์ฒ˜๋ฆฌ\n(์ฆ‰์‹œ ์„ฑ๊ณต) + Dist -> EventDB: ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(์šฐ๋ฆฌ๋™๋„คTV, ์„ฑ๊ณต,\n๋ฐฐํฌID, ์˜ˆ์ƒ๋…ธ์ถœ์ˆ˜) + end +else ๋ง๊ณ ๋น„์ฆˆ + alt ๋ง๊ณ ๋น„์ฆˆ ์„ ํƒ + Dist -> Dist: Mock ์ฒ˜๋ฆฌ\n(์ฆ‰์‹œ ์„ฑ๊ณต) + Dist -> EventDB: ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(๋ง๊ณ ๋น„์ฆˆ, ์„ฑ๊ณต,\n์—…๋ฐ์ดํŠธ ์‹œ๊ฐ) + end +else ์ง€๋‹ˆTV + alt ์ง€๋‹ˆTV ์„ ํƒ + Dist -> Dist: Mock ์ฒ˜๋ฆฌ\n(์ฆ‰์‹œ ์„ฑ๊ณต) + Dist -> EventDB: ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(์ง€๋‹ˆTV, ์„ฑ๊ณต,\n๊ด‘๊ณ ID, ์Šค์ผ€์ค„) + end +else Instagram + alt Instagram ์„ ํƒ + Dist -> Dist: Mock ์ฒ˜๋ฆฌ\n(์ฆ‰์‹œ ์„ฑ๊ณต) + Dist -> EventDB: ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(Instagram, ์„ฑ๊ณต,\npostUrl, postId) + end +else Naver Blog + alt Naver Blog ์„ ํƒ + Dist -> Dist: Mock ์ฒ˜๋ฆฌ\n(์ฆ‰์‹œ ์„ฑ๊ณต) + Dist -> EventDB: ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(NaverBlog, ์„ฑ๊ณต,\npostUrl) + end +else Kakao Channel + alt Kakao Channel ์„ ํƒ + Dist -> Dist: Mock ์ฒ˜๋ฆฌ\n(์ฆ‰์‹œ ์„ฑ๊ณต) + Dist -> EventDB: ์ฑ„๋„ ๋กœ๊ทธ ์ €์žฅ\n(KakaoChannel, ์„ฑ๊ณต,\nmessageId) + end +end + +note over Dist: ๋ชจ๋“  ์ฑ„๋„ ๋ฐฐํฌ ์™„๋ฃŒ (์ฆ‰์‹œ ์ฒ˜๋ฆฌ) + +Dist -> EventDB: ๋ฐฐํฌ ์ด๋ ฅ ์ƒํƒœ ์—…๋ฐ์ดํŠธ\n(์ƒํƒœ: COMPLETED, ์™„๋ฃŒ์ผ์‹œ) + +Dist -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, channels[], results[], completedAt} + +Dist --> Event: REST API ๋™๊ธฐ ์‘๋‹ต\n200 OK\n{distributionId, status: COMPLETED, results[]} + +Event -> EventDB: ์ด๋ฒคํŠธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ\n(APPROVED โ†’ ACTIVE๋กœ ๋ณ€๊ฒฝ) +EventDB --> Event: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ + +Event --> Gateway: 200 OK\n{eventId, ๋ฐฐํฌ๊ฒฐ๊ณผ} +Gateway --> FE: ๋ฐฐํฌ ์™„๋ฃŒ +FE --> User: "์ด๋ฒคํŠธ๊ฐ€ ๋ฐฐํฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"\n๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ + +note over Event, Dist: Sprint 2 ์ œ์•ฝ์‚ฌํ•ญ\n- ์™ธ๋ถ€ API ํ˜ธ์ถœ ์—†์Œ (Mock)\n- ๋ชจ๋“  ๋ฐฐํฌ ์ฆ‰์‹œ ์„ฑ๊ณต ์ฒ˜๋ฆฌ\n- Circuit Breaker ๋ฏธ๊ตฌํ˜„\n- Retry ๋กœ์ง ๋ฏธ๊ตฌํ˜„\n\nSprint 3 ์ดํ›„ ๊ตฌํ˜„ ์˜ˆ์ •\n- ์‹ค์ œ ์™ธ๋ถ€ ์ฑ„๋„ API ์—ฐ๋™\n- Circuit Breaker ํŒจํ„ด\n- Retry ๋ฐ ์‹คํŒจ ์ฒ˜๋ฆฌ + +@enduml diff --git a/design/pattern/architecture-pattern.md b/design/pattern/architecture-pattern.md new file mode 100644 index 0000000..243552c --- /dev/null +++ b/design/pattern/architecture-pattern.md @@ -0,0 +1,619 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ํด๋ผ์šฐ๋“œ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด ์ ์šฉ ๋ฐฉ์•ˆ (MVP) + +## ๋ฌธ์„œ ์ •๋ณด +- **์ž‘์„ฑ์ผ**: 2025-10-21 +- **๋ฒ„์ „**: 2.0 (MVP ์ง‘์ค‘) +- **์ ์šฉ ํŒจํ„ด**: 4๊ฐœ ํ•ต์‹ฌ ํŒจํ„ด +- **๋ชฉํ‘œ**: ๋น ๋ฅธ MVP ์ถœ์‹œ์™€ ์•ˆ์ •์  ์„œ๋น„์Šค ์ œ๊ณต + +--- + +## 1. ์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„ + +### 1.1 ๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ + +**User ์„œ๋น„์Šค** +- ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ๋ฐ ํ”„๋กœํ•„ ๊ด€๋ฆฌ +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ (๊ตญ์„ธ์ฒญ API) + +**Event ์„œ๋น„์Šค** +- ์ด๋ฒคํŠธ CRUD ๋ฐ ์ƒํƒœ ๊ด€๋ฆฌ +- ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ โ†’ AI ์ถ”์ฒœ โ†’ ์ฝ˜ํ…์ธ  ์ƒ์„ฑ โ†’ ๋ฐฐํฌ โ†’ ๋ถ„์„ ํ”Œ๋กœ์šฐ + +**AI ์„œ๋น„์Šค** +- ํŠธ๋ Œ๋“œ ๋ถ„์„ (์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ) +- 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ ์ž๋™ ์ƒ์„ฑ +- Claude API/GPT-4 API ์—ฐ๋™ + +**Content ์„œ๋น„์Šค** +- 3๊ฐ€์ง€ ์Šคํƒ€์ผ SNS ์ด๋ฏธ์ง€ ์ž๋™ ์ƒ์„ฑ +- Stable Diffusion/DALL-E API ์—ฐ๋™ +- ํ”Œ๋žซํผ๋ณ„ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” (Instagram, Naver, Kakao) + +**Distribution ์„œ๋น„์Šค** +- ๋‹ค์ค‘ ์ฑ„๋„ ๋™์‹œ ๋ฐฐํฌ (์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS) +- 7๊ฐœ ์™ธ๋ถ€ API ์—ฐ๋™ + +**Participation ์„œ๋น„์Šค** +- ์ด๋ฒคํŠธ ์ฐธ์—ฌ ์ ‘์ˆ˜ ๋ฐ ๋‹น์ฒจ์ž ์ถ”์ฒจ + +**Analytics ์„œ๋น„์Šค** +- ์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ ๋Œ€์‹œ๋ณด๋“œ +- ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ + +### 1.2 ๋น„๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ + +**์„ฑ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ** +- AI ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ด๋ฒคํŠธ ์ถ”์ฒœ: **10์ดˆ ์ด๋‚ด** +- SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ: **5์ดˆ ์ด๋‚ด** +- ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ: **5๋ถ„ ๊ฐ„๊ฒฉ** +- ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ: **1๋ถ„ ์ด๋‚ด** + +**๊ฐ€์šฉ์„ฑ ์š”๊ตฌ์‚ฌํ•ญ** +- ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ: **99% ์ด์ƒ** (MVP ๋ชฉํ‘œ) +- ์™ธ๋ถ€ API ์žฅ์•  ์‹œ ์„œ๋น„์Šค ์ง€์† ๊ฐ€๋Šฅ + +**ํ™•์žฅ์„ฑ ์š”๊ตฌ์‚ฌํ•ญ** +- ์ดˆ๊ธฐ ์‚ฌ์šฉ์ž: **100๋ช…** +- ๋™์‹œ ์ด๋ฒคํŠธ: **50๊ฐœ** +- ์บ์‹œ ํžˆํŠธ์œจ: **80% ์ด์ƒ** + +**๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ** +- JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ/์ธ๊ฐ€ +- API Gateway๋ฅผ ํ†ตํ•œ ์ค‘์•™์ง‘์ค‘์‹ ๋ณด์•ˆ + +### 1.3 ์™ธ๋ถ€ API ์˜์กด์„ฑ + +| ์„œ๋น„์Šค | ์™ธ๋ถ€ API | ์šฉ๋„ | ์‘๋‹ต์‹œ๊ฐ„ | +|--------|----------|------|----------| +| User | ๊ตญ์„ธ์ฒญ ์‚ฌ์—…์ž๋“ฑ๋ก์ •๋ณด ์ง„์œ„ํ™•์ธ API | ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ | 2-3์ดˆ | +| AI | Claude API / GPT-4 API | ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ | 5-10์ดˆ | +| Content | Stable Diffusion / DALL-E API | SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ | 3-5์ดˆ | +| Distribution | ์šฐ๋ฆฌ๋™๋„คTV API | ์˜์ƒ ์—…๋กœ๋“œ ๋ฐ ์†ก์ถœ | 1-2์ดˆ | +| Distribution | ๋ง๊ณ ๋น„์ฆˆ API | ์—ฐ๊ฒฐ์Œ ์—…๋ฐ์ดํŠธ | 1์ดˆ | +| Distribution | ์ง€๋‹ˆTV ๊ด‘๊ณ  API | ๊ด‘๊ณ  ๋“ฑ๋ก | 1-2์ดˆ | +| Distribution | SNS API (Instagram, Naver, Kakao) | ์ž๋™ ํฌ์ŠคํŒ… | 1-3์ดˆ | + +### 1.4 ๊ธฐ์ˆ ์  ๋„์ „๊ณผ์ œ + +#### 1. AI ์‘๋‹ต ์‹œ๊ฐ„ ๊ด€๋ฆฌ (๐Ÿ”ด Critical) + +**๋ฌธ์ œ**: AI API ์‘๋‹ต์ด 10์ดˆ ๋ชฉํ‘œ๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์žˆ์Œ +- Claude/GPT-4 API ์‘๋‹ต ์‹œ๊ฐ„ ๋ณ€๋™์„ฑ (5-15์ดˆ) +- ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ด๋ฒคํŠธ ์ถ”์ฒœ 2๋‹จ๊ณ„ ์ฒ˜๋ฆฌ ํ•„์š” + +**ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: +- **๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ**: Job ๊ธฐ๋ฐ˜ ์ฒ˜๋ฆฌ๋กœ ์‘๋‹ต ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์ตœ์†Œํ™” +- **์บ์‹ฑ**: ๋™์ผ ์กฐ๊ฑด(์—…์ข…, ์ง€์—ญ, ๋ชฉ์ ) ๊ฒฐ๊ณผ Redis ์บ์‹ฑ (24์‹œ๊ฐ„) +- **์‘๋‹ต ์‹œ๊ฐ„ 90% ๋‹จ์ถ•** (์บ์‹œ ํžˆํŠธ ์‹œ) + +#### 2. ๋‹ค์ค‘ ์™ธ๋ถ€ API ์˜์กด์„ฑ (๐Ÿ”ด Critical) + +**๋ฌธ์ œ**: 7๊ฐœ ์™ธ๋ถ€ API ์—ฐ๋™์œผ๋กœ ์ธํ•œ ์žฅ์•  ์ „ํŒŒ ์œ„ํ—˜ +- ๊ฐ API๋ณ„ ์‘๋‹ต ์‹œ๊ฐ„ ๋ฐ ์„ฑ๊ณต๋ฅ  ์ƒ์ด +- ํ•˜๋‚˜์˜ API ์žฅ์• ๊ฐ€ ์ „์ฒด ์„œ๋น„์Šค ์ค‘๋‹จ์œผ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์Œ + +**ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: +- **Circuit Breaker**: ์žฅ์•  API ์ž๋™ ์ฐจ๋‹จ ๋ฐ Fallback ์ฒ˜๋ฆฌ +- **๋…๋ฆฝ์  ์ฒ˜๋ฆฌ**: ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ์‹คํŒจ๊ฐ€ ๋‹ค๋ฅธ ์ฑ„๋„์— ์˜ํ–ฅ ์—†์Œ +- **์ž๋™ ์žฌ์‹œ๋„**: ์ผ์‹œ์  ์˜ค๋ฅ˜ ์‹œ ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„ + +#### 3. ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋น„์šฉ ๋ฐ ์‹œ๊ฐ„ (๐ŸŸก Important) + +**๋ฌธ์ œ**: AI ์ด๋ฏธ์ง€ ์ƒ์„ฑ API ๋น„์šฉ์ด ๋†’๊ณ  ์‹œ๊ฐ„ ์†Œ์š” +- ์ด๋ฏธ์ง€ 1์žฅ๋‹น ์ƒ์„ฑ ๋น„์šฉ: $0.02-0.05 +- 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์ƒ์„ฑ ์‹œ ๋น„์šฉ 3๋ฐฐ + +**ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: +- **์บ์‹ฑ**: ๋™์ผ ์ด๋ฒคํŠธ ์ •๋ณด ์ด๋ฏธ์ง€ ์žฌ์‚ฌ์šฉ +- **๋น„์šฉ 90% ์ ˆ๊ฐ** (์บ์‹œ ํžˆํŠธ ์‹œ) + +#### 4. ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ์„ฑ๋Šฅ (๐ŸŸก Important) + +**๋ฌธ์ œ**: ๋‹ค์ค‘ ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ†ตํ•ฉ์œผ๋กœ ์ธํ•œ ์‘๋‹ต ์ง€์—ฐ +- 7๊ฐœ ์™ธ๋ถ€ API + ๋‚ด๋ถ€ ์„œ๋น„์Šค ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ +- ๋ณต์žกํ•œ ์ฐจํŠธ ๋ฐ ๊ณ„์‚ฐ ๋กœ์ง + +**ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: +- **์บ์‹ฑ**: Redis๋ฅผ ํ†ตํ•œ 5๋ถ„ ๊ฐ„๊ฒฉ ๋ฐ์ดํ„ฐ ์บ์‹ฑ +- **์‘๋‹ต ์‹œ๊ฐ„ 80% ๋‹จ์ถ•** + +--- + +## 2. ํŒจํ„ด ์„ ์ • ๋ฐ ํ‰๊ฐ€ + +### 2.1 MVP ํ•ต์‹ฌ ํŒจํ„ด (4๊ฐœ) + +| ํŒจํ„ด | ์ ์šฉ ๋ชฉ์  | ์ฃผ์š” ํšจ๊ณผ | +|------|----------|-----------| +| **Cache-Aside** | AI ์‘๋‹ต ์‹œ๊ฐ„ ๋‹จ์ถ• ๋ฐ ๋น„์šฉ ์ ˆ๊ฐ | ์‘๋‹ต ์‹œ๊ฐ„ 90% ๋‹จ์ถ•, ๋น„์šฉ 90% ์ ˆ๊ฐ | +| **API Gateway** | ์ค‘์•™์ง‘์ค‘์‹ ๋ณด์•ˆ ๋ฐ ๋ผ์šฐํŒ… | ๊ฐœ๋ฐœ ๋ณต์žก๋„ ๊ฐ์†Œ, ๋ณด์•ˆ ๊ฐ•ํ™” | +| **Asynchronous Request-Reply** | ์žฅ์‹œ๊ฐ„ ์ž‘์—… ์‘๋‹ต ์‹œ๊ฐ„ ๊ฐœ์„  | ์‚ฌ์šฉ์ž ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์ œ๊ฑฐ | +| **Circuit Breaker** (Optional) | ์™ธ๋ถ€ API ์žฅ์•  ๊ฒฉ๋ฆฌ | ๊ฐ€์šฉ์„ฑ 95% โ†’ 99% ๊ฐœ์„  | + +### 2.2 ์ •๋Ÿ‰์  ํ‰๊ฐ€ ๋งคํŠธ๋ฆญ์Šค + +**ํ‰๊ฐ€ ๊ธฐ์ค€**: +- **๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ** (35%): ์š”๊ตฌ์‚ฌํ•ญ ์ง์ ‘ ํ•ด๊ฒฐ ๋Šฅ๋ ฅ +- **์„ฑ๋Šฅ ํšจ๊ณผ** (25%): ์‘๋‹ต์‹œ๊ฐ„ ๋ฐ ์ฒ˜๋ฆฌ๋Ÿ‰ ๊ฐœ์„  +- **์šด์˜ ๋ณต์žก๋„** (20%): ๊ตฌํ˜„ ๋ฐ ์šด์˜ ์šฉ์ด์„ฑ +- **ํ™•์žฅ์„ฑ** (15%): ๋ฏธ๋ž˜ ์š”๊ตฌ์‚ฌํ•ญ ๋Œ€์‘๋ ฅ +- **๋น„์šฉ ํšจ์œจ์„ฑ** (5%): ๊ฐœ๋ฐœ/์šด์˜ ๋น„์šฉ ๋Œ€๋น„ ํšจ๊ณผ + +| ํŒจํ„ด | ๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ
(35%) | ์„ฑ๋Šฅ ํšจ๊ณผ
(25%) | ์šด์˜ ๋ณต์žก๋„
(20%) | ํ™•์žฅ์„ฑ
(15%) | ๋น„์šฉ ํšจ์œจ์„ฑ
(5%) | **์ด์ ** | ์šฐ์„ ์ˆœ์œ„ | +|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| **Cache-Aside** | 10 ร— 0.35
= 3.5 | 10 ร— 0.25
= 2.5 | 8 ร— 0.20
= 1.6 | 9 ร— 0.15
= 1.35 | 10 ร— 0.05
= 0.5 | **9.45** | ๐Ÿ”ด Critical | +| **API Gateway** | 9 ร— 0.35
= 3.15 | 7 ร— 0.25
= 1.75 | 9 ร— 0.20
= 1.8 | 9 ร— 0.15
= 1.35 | 8 ร— 0.05
= 0.4 | **8.45** | ๐Ÿ”ด Critical | +| **Asynchronous Request-Reply** | 9 ร— 0.35
= 3.15 | 9 ร— 0.25
= 2.25 | 7 ร— 0.20
= 1.4 | 8 ร— 0.15
= 1.2 | 8 ร— 0.05
= 0.4 | **8.40** | ๐Ÿ”ด Critical | +| **Circuit Breaker** | 8 ร— 0.35
= 2.8 | 8 ร— 0.25
= 2.0 | 9 ร— 0.20
= 1.8 | 9 ร— 0.15
= 1.35 | 7 ร— 0.05
= 0.35 | **8.30** | ๐ŸŸก Optional | + +### 2.3 ํŒจํ„ด๋ณ„ ์ƒ์„ธ ๋ถ„์„ + +#### 2.3.1 Cache-Aside (9.45์ ) - ๐Ÿ”ด Critical + +**์ ์šฉ ๋Œ€์ƒ**: +- AI ์„œ๋น„์Šค: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ, ์ด๋ฒคํŠธ ์ถ”์ฒœ ๊ฒฐ๊ณผ +- Content ์„œ๋น„์Šค: ์ƒ์„ฑ๋œ SNS ์ด๋ฏธ์ง€ +- User ์„œ๋น„์Šค: ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ๊ฒฐ๊ณผ + +**๊ตฌํ˜„ ๋ฐฉ์‹**: +``` +1. ์š”์ฒญ ์ˆ˜์‹  โ†’ ์บ์‹œ ํ™•์ธ (Redis GET) +2. ์บ์‹œ HIT: ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (์‘๋‹ต ์‹œ๊ฐ„ 0.1์ดˆ) +3. ์บ์‹œ MISS: + - ์™ธ๋ถ€ API ํ˜ธ์ถœ (์‘๋‹ต ์‹œ๊ฐ„ 5-10์ดˆ) + - ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œ์— ์ €์žฅ (Redis SET, TTL 24์‹œ๊ฐ„) + - ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ +``` + +**๊ธฐ๋Œ€ ํšจ๊ณผ**: +- **์‘๋‹ต ์‹œ๊ฐ„**: 10์ดˆ โ†’ 0.1์ดˆ (99% ๊ฐœ์„ , ์บ์‹œ ํžˆํŠธ ์‹œ) +- **๋น„์šฉ ์ ˆ๊ฐ**: AI API ํ˜ธ์ถœ 90% ๊ฐ์†Œ โ†’ ์›” $2,000 โ†’ $200 (90% ์ ˆ๊ฐ) +- **์บ์‹œ ํžˆํŠธ์œจ**: 80% (๋™์ผ ์—…์ข…/์ง€์—ญ ์ด๋ฒคํŠธ ๋ฐ˜๋ณต ์š”์ฒญ) + +**์šด์˜ ๊ณ ๋ ค์‚ฌํ•ญ**: +- Redis ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ: ์ตœ๋Œ€ 2GB (์•ฝ 10,000๊ฐœ ์บ์‹œ ํ•ญ๋ชฉ) +- TTL ์ •์ฑ…: 24์‹œ๊ฐ„ (ํŠธ๋ Œ๋“œ ๋ณ€ํ™” ๋ฐ˜์˜) +- ์บ์‹œ ๋ฌดํšจํ™”: ์ˆ˜๋™ ๋ฌดํšจํ™” API ์ œ๊ณต + +#### 2.3.2 API Gateway (8.45์ ) - ๐Ÿ”ด Critical + +**์ ์šฉ ๋Œ€์ƒ**: +- ์ „์ฒด ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค (User, Event, AI, Content, Distribution, Participation, Analytics) + +**์ฃผ์š” ๊ธฐ๋Šฅ**: +1. **์ธ์ฆ/์ธ๊ฐ€**: JWT ํ† ํฐ ๊ฒ€์ฆ (๋ชจ๋“  ์š”์ฒญ) +2. **๋ผ์šฐํŒ…**: URL ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค ๋ผ์šฐํŒ… +3. **Rate Limiting**: API ํ˜ธ์ถœ ์ œํ•œ (์‚ฌ์šฉ์ž๋‹น 100 req/min) +4. **๋กœ๊น…**: ์ค‘์•™์ง‘์ค‘์‹ ์ ‘๊ทผ ๋กœ๊ทธ + +**๊ตฌํ˜„ ๋ฐฉ์‹**: +``` +Client โ†’ API Gateway (Kong/AWS API Gateway) + โ”œโ”€ /api/users/* โ†’ User Service + โ”œโ”€ /api/events/* โ†’ Event Service + โ”œโ”€ /api/ai/* โ†’ AI Service + โ”œโ”€ /api/content/* โ†’ Content Service + โ”œโ”€ /api/distribution/* โ†’ Distribution Service + โ”œโ”€ /api/participation/* โ†’ Participation Service + โ””โ”€ /api/analytics/* โ†’ Analytics Service +``` + +**๊ธฐ๋Œ€ ํšจ๊ณผ**: +- **๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ**: ๊ฐ ์„œ๋น„์Šค์—์„œ ์ธ์ฆ ๋กœ์ง ์ œ๊ฑฐ โ†’ ๊ฐœ๋ฐœ ์‹œ๊ฐ„ 30% ๋‹จ์ถ• +- **๋ณด์•ˆ ๊ฐ•ํ™”**: ์ค‘์•™์ง‘์ค‘์‹ ๋ณด์•ˆ ์ •์ฑ… ๊ด€๋ฆฌ +- **์šด์˜ ํšจ์œจ**: ํ†ตํ•ฉ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น… + +#### 2.3.3 Asynchronous Request-Reply (8.40์ ) - ๐Ÿ”ด Critical + +**์ ์šฉ ๋Œ€์ƒ**: +- AI ์„œ๋น„์Šค: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ (10์ดˆ ์†Œ์š”) +- Content ์„œ๋น„์Šค: SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ (5์ดˆ ์†Œ์š”) + +**๊ตฌํ˜„ ๋ฐฉ์‹**: +``` +1. ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ โ†’ Job ID ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (์‘๋‹ต ์‹œ๊ฐ„ 0.1์ดˆ) +2. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ: + - AI API ํ˜ธ์ถœ ๋ฐ ๊ฒฐ๊ณผ ์ƒ์„ฑ (10์ดˆ) + - ๊ฒฐ๊ณผ๋ฅผ DB ์ €์žฅ +3. ํด๋ผ์ด์–ธํŠธ ํด๋ง: + - 5์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ Job ์ƒํƒœ ํ™•์ธ (GET /jobs/{id}) + - ์™„๋ฃŒ ์‹œ ๊ฒฐ๊ณผ ์ˆ˜์‹  +``` + +**๊ธฐ๋Œ€ ํšจ๊ณผ**: +- **์‚ฌ์šฉ์ž ๊ฒฝํ—˜**: ๋Œ€๊ธฐ ํ™”๋ฉด์—์„œ ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ โ†’ ์ดํƒˆ๋ฅ  ๊ฐ์†Œ +- **์„œ๋ฒ„ ๋ถ€ํ•˜**: ๋™๊ธฐ ๋Œ€๊ธฐ ์ œ๊ฑฐ โ†’ ๋™์‹œ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ ์š”์ฒญ 5๋ฐฐ ์ฆ๊ฐ€ + +**์šด์˜ ๊ณ ๋ ค์‚ฌํ•ญ**: +- Job ์ƒํƒœ ์ €์žฅ: Redis (TTL 1์‹œ๊ฐ„) +- ํด๋ง ๊ฐ„๊ฒฉ: 5์ดˆ (UX ๊ณ ๋ ค) +- Job ๋งŒ๋ฃŒ: 1์‹œ๊ฐ„ ํ›„ ์ž๋™ ์‚ญ์ œ + +#### 2.3.4 Circuit Breaker (8.30์ ) - ๐ŸŸก Optional + +**์ ์šฉ ๋Œ€์ƒ**: +- ๋ชจ๋“  ์™ธ๋ถ€ API ์—ฐ๋™ ์ง€์  (7๊ฐœ API) + +**๋™์ž‘ ๋ฐฉ์‹**: +``` +1. Closed (์ •์ƒ): + - ์™ธ๋ถ€ API ์ •์ƒ ํ˜ธ์ถœ + - ์‹คํŒจ์œจ 5% ๋ฏธ๋งŒ + +2. Open (์ฐจ๋‹จ): + - ์‹คํŒจ์œจ 5% ์ดˆ๊ณผ ์‹œ Circuit Open + - ๋ชจ๋“  ์š”์ฒญ ์ฆ‰์‹œ ์‹คํŒจ (Fallback ์ฒ˜๋ฆฌ) + - 30์ดˆ ๋Œ€๊ธฐ + +3. Half-Open (ํ…Œ์ŠคํŠธ): + - 30์ดˆ ํ›„ 1๊ฐœ ์š”์ฒญ ์‹œ๋„ + - ์„ฑ๊ณต ์‹œ Closed๋กœ ์ „ํ™˜ + - ์‹คํŒจ ์‹œ Open ์œ ์ง€ +``` + +**Fallback ์ „๋žต**: +- **AI ์„œ๋น„์Šค**: ์บ์‹œ๋œ ์ด์ „ ์ถ”์ฒœ ๊ฒฐ๊ณผ ์ œ๊ณต + ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ +- **Distribution ์„œ๋น„์Šค**: ํ•ด๋‹น ์ฑ„๋„ ๋ฐฐํฌ ์Šคํ‚ต + ์•Œ๋ฆผ +- **User ์„œ๋น„์Šค**: ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ์Šคํ‚ต (์ˆ˜๋™ ํ™•์ธ์œผ๋กœ ๋Œ€์ฒด) + +**๊ธฐ๋Œ€ ํšจ๊ณผ**: +- **๊ฐ€์šฉ์„ฑ**: 95% โ†’ 99% (์™ธ๋ถ€ API ์žฅ์•  ๊ฒฉ๋ฆฌ) +- **์žฅ์•  ๋ณต๊ตฌ**: ์ž๋™ ๋ณต๊ตฌ ์‹œ๊ฐ„ 5๋ถ„ โ†’ 30์ดˆ + +--- + +## 3. ์„œ๋น„์Šค๋ณ„ ํŒจํ„ด ์ ์šฉ ์„ค๊ณ„ + +### 3.1 ์ „์ฒด ์•„ํ‚คํ…์ฒ˜ (MVP) + +```mermaid +graph TB + subgraph "ํด๋ผ์ด์–ธํŠธ" + Client[Web/Mobile App] + end + + subgraph "API Gateway Layer" + Gateway[API Gateway
์ธ์ฆ, ๋ผ์šฐํŒ…, Rate Limiting] + end + + subgraph "๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค" + UserSvc[User Service
ํšŒ์›๊ด€๋ฆฌ] + EventSvc[Event Service
์ด๋ฒคํŠธ ๊ด€๋ฆฌ] + AISvc[AI Service
ํŠธ๋ Œ๋“œ ๋ถ„์„, ์ถ”์ฒœ] + ContentSvc[Content Service
์ด๋ฏธ์ง€ ์ƒ์„ฑ] + DistSvc[Distribution Service
๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ] + PartSvc[Participation Service
์ฐธ์—ฌ์ž ๊ด€๋ฆฌ] + AnalSvc[Analytics Service
์„ฑ๊ณผ ๋ถ„์„] + end + + subgraph "Cache Layer" + Redis[(Redis Cache
Cache-Aside)] + end + + subgraph "์™ธ๋ถ€ API (Circuit Breaker ์ ์šฉ)" + TaxAPI[๊ตญ์„ธ์ฒญ API] + ClaudeAPI[Claude API] + SDAPI[Stable Diffusion] + UriAPI[์šฐ๋ฆฌ๋™๋„คTV API] + RingoAPI[๋ง๊ณ ๋น„์ฆˆ API] + GenieAPI[์ง€๋‹ˆTV API] + SNSAPI[SNS API] + end + + Client -->|HTTPS| Gateway + Gateway --> UserSvc + Gateway --> EventSvc + Gateway --> AISvc + Gateway --> ContentSvc + Gateway --> DistSvc + Gateway --> PartSvc + Gateway --> AnalSvc + + UserSvc -.->|Cache-Aside| Redis + AISvc -.->|Cache-Aside| Redis + ContentSvc -.->|Cache-Aside| Redis + + UserSvc -->|Circuit Breaker| TaxAPI + AISvc -->|Circuit Breaker| ClaudeAPI + ContentSvc -->|Circuit Breaker| SDAPI + DistSvc -->|Circuit Breaker| UriAPI + DistSvc -->|Circuit Breaker| RingoAPI + DistSvc -->|Circuit Breaker| GenieAPI + DistSvc -->|Circuit Breaker| SNSAPI + + style Gateway fill:#e1f5ff + style Redis fill:#ffe1e1 + style AISvc fill:#fff4e1 + style ContentSvc fill:#fff4e1 +``` + +### 3.2 AI Service - Asynchronous Request-Reply ํŒจํ„ด + +```mermaid +sequenceDiagram + participant Client as ํด๋ผ์ด์–ธํŠธ + participant API as API Gateway + participant Event as Event Service + participant AI as AI Service + participant Cache as Redis Cache + participant Claude as Claude API + + Client->>API: POST /api/ai/recommendations
(์—…์ข…, ์ง€์—ญ, ๋ชฉ์ ) + API->>Event: ์ด๋ฒคํŠธ ์ถ”์ฒœ ์š”์ฒญ + Event->>AI: ์ถ”์ฒœ ์š”์ฒญ + + AI->>Cache: GET cached_result + alt Cache HIT + Cache-->>AI: ์บ์‹œ๋œ ๊ฒฐ๊ณผ + AI-->>Event: ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (0.1์ดˆ) + Event-->>API: ๊ฒฐ๊ณผ + API-->>Client: ๊ฒฐ๊ณผ (Total: 0.2์ดˆ) + else Cache MISS + Cache-->>AI: null + AI-->>Event: Job ID ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + Event-->>API: Job ID + API-->>Client: Job ID + ์ฒ˜๋ฆฌ์ค‘ ์ƒํƒœ (0.1์ดˆ) + + AI->>Claude: ๋น„๋™๊ธฐ AI ํ˜ธ์ถœ + Note over AI,Claude: ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ (10์ดˆ) + Claude-->>AI: ์ถ”์ฒœ ๊ฒฐ๊ณผ + AI->>Cache: SET result (TTL 24h) + AI->>AI: Job ์ƒํƒœ = ์™„๋ฃŒ + + loop ํด๋ง (5์ดˆ ๊ฐ„๊ฒฉ) + Client->>API: GET /api/jobs/{id} + API->>Event: Job ์ƒํƒœ ํ™•์ธ + Event->>AI: Job ์ƒํƒœ + alt ์™„๋ฃŒ + AI-->>Event: ๊ฒฐ๊ณผ + Event-->>API: ๊ฒฐ๊ณผ + API-->>Client: ์ตœ์ข… ๊ฒฐ๊ณผ + else ์ง„ํ–‰์ค‘ + AI-->>Event: ์ง„ํ–‰์ค‘ + Event-->>API: ์ง„ํ–‰์ค‘ + API-->>Client: ์ง„ํ–‰ ์ƒํ™ฉ (์˜ˆ: 70%) + end + end + end +``` + +### 3.3 Distribution Service - Circuit Breaker ํŒจํ„ด + +```mermaid +graph TB + subgraph "Distribution Service" + DistCtrl[Distribution Controller] + CB_Uri[Circuit Breaker
์šฐ๋ฆฌ๋™๋„คTV] + CB_Ringo[Circuit Breaker
๋ง๊ณ ๋น„์ฆˆ] + CB_Genie[Circuit Breaker
์ง€๋‹ˆTV] + CB_SNS[Circuit Breaker
SNS] + end + + subgraph "์™ธ๋ถ€ API" + UriAPI[์šฐ๋ฆฌ๋™๋„คTV API] + RingoAPI[๋ง๊ณ ๋น„์ฆˆ API] + GenieAPI[์ง€๋‹ˆTV API] + SNSAPI[SNS API] + end + + DistCtrl --> CB_Uri + DistCtrl --> CB_Ringo + DistCtrl --> CB_Genie + DistCtrl --> CB_SNS + + CB_Uri -->|์‹คํŒจ์œจ < 5%
Closed| UriAPI + CB_Uri -.->|์‹คํŒจ์œจ >= 5%
Open, Fallback| Fallback_Uri[๋ฐฐํฌ ์Šคํ‚ต
+ ์•Œ๋ฆผ] + + CB_Ringo -->|์‹คํŒจ์œจ < 5%
Closed| RingoAPI + CB_Ringo -.->|์‹คํŒจ์œจ >= 5%
Open, Fallback| Fallback_Ringo[๋ฐฐํฌ ์Šคํ‚ต
+ ์•Œ๋ฆผ] + + CB_Genie -->|์‹คํŒจ์œจ < 5%
Closed| GenieAPI + CB_Genie -.->|์‹คํŒจ์œจ >= 5%
Open, Fallback| Fallback_Genie[๋ฐฐํฌ ์Šคํ‚ต
+ ์•Œ๋ฆผ] + + CB_SNS -->|์‹คํŒจ์œจ < 5%
Closed| SNSAPI + CB_SNS -.->|์‹คํŒจ์œจ >= 5%
Open, Fallback| Fallback_SNS[๋ฐฐํฌ ์Šคํ‚ต
+ ์•Œ๋ฆผ] + + style CB_Uri fill:#ffe1e1 + style CB_Ringo fill:#ffe1e1 + style CB_Genie fill:#ffe1e1 + style CB_SNS fill:#ffe1e1 +``` + +--- + +## 4. MVP ๊ตฌํ˜„ ๋กœ๋“œ๋งต + +### 4.1 ๋‹จ์ผ Phase MVP ์ „๋žต + +**๋ชฉํ‘œ**: ๋น ๋ฅธ ์ถœ์‹œ์™€ ์•ˆ์ •์  ์„œ๋น„์Šค ์ œ๊ณต + +**๊ธฐ๊ฐ„**: 12์ฃผ + +**๋ชฉํ‘œ ์ง€ํ‘œ**: +- ์‚ฌ์šฉ์ž: 100๋ช… +- ๋™์‹œ ์ด๋ฒคํŠธ: 50๊ฐœ +- ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ: 99% +- AI ์‘๋‹ต ์‹œ๊ฐ„: 10์ดˆ ์ด๋‚ด (์บ์‹œ ๋ฏธ์Šค), 0.1์ดˆ (์บ์‹œ ํžˆํŠธ) + +### 4.2 ๊ตฌํ˜„ ์ˆœ์„œ ๋ฐ ์ผ์ • + +| ์ฃผ์ฐจ | ์ž‘์—… ๋‚ด์šฉ | ํŒจํ„ด ์ ์šฉ | ์™„๋ฃŒ ๊ธฐ์ค€ | +|------|----------|-----------|-----------| +| **1-2์ฃผ** | ์ธํ”„๋ผ ๊ตฌ์ถ• | API Gateway | - Kong/AWS API Gateway ์„ค์ •
- Redis ํด๋Ÿฌ์Šคํ„ฐ ๊ตฌ์ถ•
- JWT ์ธ์ฆ ๊ตฌํ˜„ | +| **3-4์ฃผ** | User/Event ์„œ๋น„์Šค | Cache-Aside | - ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ์™„๋ฃŒ
- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ์บ์‹ฑ
- ์ด๋ฒคํŠธ CRUD ์™„๋ฃŒ | +| **5-6์ฃผ** | AI ์„œ๋น„์Šค | Asynchronous Request-Reply
Cache-Aside | - Claude API ์—ฐ๋™
- Job ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
- AI ๊ฒฐ๊ณผ ์บ์‹ฑ (24์‹œ๊ฐ„ TTL) | +| **7-8์ฃผ** | Content ์„œ๋น„์Šค | Asynchronous Request-Reply
Cache-Aside | - Stable Diffusion ์—ฐ๋™
- ์ด๋ฏธ์ง€ ์ƒ์„ฑ Job ์ฒ˜๋ฆฌ
- ์ด๋ฏธ์ง€ ์บ์‹ฑ ๋ฐ CDN | +| **9-10์ฃผ** | Distribution ์„œ๋น„์Šค | Circuit Breaker | - 7๊ฐœ ์™ธ๋ถ€ API ์—ฐ๋™
- ๊ฐ API๋ณ„ Circuit Breaker
- Fallback ์ „๋žต ๊ตฌํ˜„ | +| **11์ฃผ** | Participation/Analytics | Cache-Aside | - ์ฐธ์—ฌ์ž ๊ด€๋ฆฌ ๋ฐ ์ถ”์ฒจ
- ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ์บ์‹ฑ | +| **12์ฃผ** | ํ…Œ์ŠคํŠธ ๋ฐ ์ถœ์‹œ | ์ „์ฒด ํŒจํ„ด ๊ฒ€์ฆ | - ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ (100๋ช…)
- ์žฅ์•  ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ
- MVP ์ถœ์‹œ | + +### 4.3 ๊ธฐ์ˆ  ์Šคํƒ + +**๋ฐฑ์—”๋“œ**: +- Spring Boot 3.2 (Java 17) / Node.js 20 +- Redis 7.2 (Cache) +- PostgreSQL 15 (์ฃผ DB) +- RabbitMQ / Kafka (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ, Optional) + +**ํ”„๋ก ํŠธ์—”๋“œ**: +- React 18 + TypeScript 5 +- Next.js 14 (SSR) +- Zustand (์ƒํƒœ๊ด€๋ฆฌ) +- React Query (์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌ) + +**์ธํ”„๋ผ**: +- Docker + Kubernetes +- Kong API Gateway / AWS API Gateway +- Resilience4j (Circuit Breaker) +- Prometheus + Grafana (๋ชจ๋‹ˆํ„ฐ๋ง) + +**์™ธ๋ถ€ API**: +- Claude API / GPT-4 API +- Stable Diffusion API +- ๊ตญ์„ธ์ฒญ ์‚ฌ์—…์ž๋“ฑ๋ก์ •๋ณด ์ง„์œ„ํ™•์ธ API +- ์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS API + +--- + +## 5. ์˜ˆ์ƒ ์„ฑ๊ณผ + +### 5.1 ์„ฑ๋Šฅ ๊ฐœ์„  + +| ํ•ญ๋ชฉ | Before | After | ๊ฐœ์„ ์œจ | +|------|--------|-------|--------| +| AI ์‘๋‹ต ์‹œ๊ฐ„ (์บ์‹œ ํžˆํŠธ) | 10์ดˆ | 0.1์ดˆ | **99% ๊ฐœ์„ ** | +| AI ์‘๋‹ต ์‹œ๊ฐ„ (์บ์‹œ ๋ฏธ์Šค) | 10์ดˆ | 10์ดˆ | - | +| ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ๊ฐ„ (์บ์‹œ ํžˆํŠธ) | 5์ดˆ | 0.1์ดˆ | **98% ๊ฐœ์„ ** | +| ๋Œ€์‹œ๋ณด๋“œ ๋กœ๋”ฉ ์‹œ๊ฐ„ | 3์ดˆ | 0.5์ดˆ | **83% ๊ฐœ์„ ** | + +### 5.2 ๋น„์šฉ ์ ˆ๊ฐ + +**AI API ๋น„์šฉ ์ ˆ๊ฐ** (์บ์‹œ ํžˆํŠธ์œจ 80% ๊ฐ€์ •): +- Before: 1,000 ์š”์ฒญ/์ผ ร— $0.02 = **$20/์ผ** = **$600/์›”** +- After: 200 ์š”์ฒญ/์ผ ร— $0.02 = **$4/์ผ** = **$120/์›”** +- **์ ˆ๊ฐ์•ก**: $480/์›” (**80% ์ ˆ๊ฐ**) + +**์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋น„์šฉ ์ ˆ๊ฐ** (์บ์‹œ ํžˆํŠธ์œจ 80% ๊ฐ€์ •): +- Before: 500 ์ด๋ฏธ์ง€/์ผ ร— $0.04 = **$20/์ผ** = **$600/์›”** +- After: 100 ์ด๋ฏธ์ง€/์ผ ร— $0.04 = **$4/์ผ** = **$120/์›”** +- **์ ˆ๊ฐ์•ก**: $480/์›” (**80% ์ ˆ๊ฐ**) + +**์ด ๋น„์šฉ ์ ˆ๊ฐ**: +- Before: $1,200/์›” +- After: $240/์›” +- **์ด ์ ˆ๊ฐ์•ก**: **$960/์›”** (**80% ์ ˆ๊ฐ**) + +### 5.3 ๊ฐ€์šฉ์„ฑ ๊ฐœ์„  + +| ์ง€ํ‘œ | Before | After | ๊ฐœ์„  | +|------|--------|-------|------| +| ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ | 95% | 99% | **+4%p** | +| ์™ธ๋ถ€ API ์žฅ์•  ์‹œ ์„œ๋น„์Šค ์ง€์† | โŒ ๋ถˆ๊ฐ€ | โœ… ๊ฐ€๋Šฅ (Fallback) | - | +| ์žฅ์•  ์ž๋™ ๋ณต๊ตฌ ์‹œ๊ฐ„ | 5๋ถ„ (์ˆ˜๋™) | 30์ดˆ (์ž๋™) | **90% ๋‹จ์ถ•** | + +### 5.4 ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ + +| ํ•ญ๋ชฉ | ํšจ๊ณผ | +|------|------| +| API Gateway | ๊ฐ ์„œ๋น„์Šค ์ธ์ฆ ๋กœ์ง ์ œ๊ฑฐ โ†’ ๊ฐœ๋ฐœ ์‹œ๊ฐ„ **30% ๋‹จ์ถ•** | +| Cache-Aside | ๋ฐ˜๋ณต API ํ˜ธ์ถœ ์ œ๊ฑฐ โ†’ ํ…Œ์ŠคํŠธ ์‹œ๊ฐ„ **50% ๋‹จ์ถ•** | +| Circuit Breaker | ์žฅ์•  ์ฒ˜๋ฆฌ ๋กœ์ง ์ž๋™ํ™” โ†’ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ฝ”๋“œ **40% ๊ฐ์†Œ** | +| Async Request-Reply | ๋™์‹œ ์ฒ˜๋ฆฌ ๋Šฅ๋ ฅ **5๋ฐฐ ์ฆ๊ฐ€** | + +--- + +## 6. ์šด์˜ ๊ณ ๋ ค์‚ฌํ•ญ + +### 6.1 ๋ชจ๋‹ˆํ„ฐ๋ง ์ง€ํ‘œ + +**Cache-Aside**: +- ์บ์‹œ ํžˆํŠธ์œจ (๋ชฉํ‘œ: 80% ์ด์ƒ) +- Redis ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋ฅ  (๋ชฉํ‘œ: 70% ์ดํ•˜) +- ์บ์‹œ ์‘๋‹ต ์‹œ๊ฐ„ (๋ชฉํ‘œ: 100ms ์ดํ•˜) + +**API Gateway**: +- ์š”์ฒญ ์ฒ˜๋ฆฌ๋Ÿ‰ (TPS) +- ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ +- ์ธ์ฆ ์‹คํŒจ์œจ + +**Circuit Breaker**: +- API๋ณ„ ์‹คํŒจ์œจ +- Circuit ์ƒํƒœ (Closed/Open/Half-Open) +- Fallback ํ˜ธ์ถœ ํšŸ์ˆ˜ + +**Asynchronous Request-Reply**: +- Job ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ +- ๋™์‹œ Job ์ˆ˜ +- Job ์™„๋ฃŒ์œจ + +### 6.2 ์•Œ๋žŒ ์ž„๊ณ„๊ฐ’ + +| ์ง€ํ‘œ | Warning | Critical | +|------|---------|----------| +| ์บ์‹œ ํžˆํŠธ์œจ | < 70% | < 50% | +| Redis ๋ฉ”๋ชจ๋ฆฌ | > 80% | > 90% | +| API ์‘๋‹ต ์‹œ๊ฐ„ | > 500ms | > 1000ms | +| Circuit Breaker Open | 1๊ฐœ | 3๊ฐœ ์ด์ƒ | +| ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ | < 99% | < 95% | + +### 6.3 ์žฅ์•  ๋Œ€์‘ ์ ˆ์ฐจ + +**Circuit Breaker Open ๋ฐœ์ƒ ์‹œ**: +1. ์•Œ๋žŒ ์ˆ˜์‹  (Slack/Email) +2. ํ•ด๋‹น ์™ธ๋ถ€ API ์ƒํƒœ ํ™•์ธ +3. Fallback ์ „๋žต ๋™์ž‘ ํ™•์ธ +4. 30์ดˆ ํ›„ ์ž๋™ ๋ณต๊ตฌ ํ™•์ธ +5. ๋ณต๊ตฌ ์‹คํŒจ ์‹œ ์ˆ˜๋™ ๊ฐœ์ž… + +**์บ์‹œ ์žฅ์•  ์‹œ**: +1. Redis ํด๋Ÿฌ์Šคํ„ฐ ์ƒํƒœ ํ™•์ธ +2. Failover ์ž๋™ ์ˆ˜ํ–‰ (Sentinel) +3. ์บ์‹œ ๋ฏธ์Šค๋กœ ์ „ํ™˜ (์„ฑ๋Šฅ ์ €ํ•˜ ํ—ˆ์šฉ) +4. ๊ธด๊ธ‰ Redis ๋ณต๊ตฌ + +--- + +## 7. ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 7.1 ํŒจํ„ด ์ ์šฉ ์™„๋ฃŒ ํ™•์ธ + +- [x] **Cache-Aside**: AI ๊ฒฐ๊ณผ, ์ด๋ฏธ์ง€, ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ์บ์‹ฑ ์™„๋ฃŒ +- [x] **API Gateway**: ์ธ์ฆ, ๋ผ์šฐํŒ…, Rate Limiting ๊ตฌํ˜„ ์™„๋ฃŒ +- [x] **Asynchronous Request-Reply**: AI ๋ฐ ์ด๋ฏธ์ง€ ์ƒ์„ฑ Job ์ฒ˜๋ฆฌ ์™„๋ฃŒ +- [x] **Circuit Breaker**: 7๊ฐœ ์™ธ๋ถ€ API์— Circuit Breaker ๋ฐ Fallback ์ ์šฉ ์™„๋ฃŒ + +### 7.2 ์„ฑ๋Šฅ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํ™•์ธ + +- [ ] AI ์‘๋‹ต ์‹œ๊ฐ„: 10์ดˆ ์ด๋‚ด (์บ์‹œ ๋ฏธ์Šค), 0.1์ดˆ (์บ์‹œ ํžˆํŠธ) +- [ ] ์บ์‹œ ํžˆํŠธ์œจ: 80% ์ด์ƒ +- [ ] ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ: 99% ์ด์ƒ +- [ ] ๋น„์šฉ ์ ˆ๊ฐ: ์›” $960 (80%) + +### 7.3 ์šด์˜ ์ค€๋น„ ์™„๋ฃŒ ํ™•์ธ + +- [ ] ๋ชจ๋‹ˆํ„ฐ๋ง ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์ถ• (Prometheus + Grafana) +- [ ] ์•Œ๋žŒ ์„ค์ • ์™„๋ฃŒ (Slack/Email) +- [ ] ์žฅ์•  ๋Œ€์‘ ๋งค๋‰ด์–ผ ์ž‘์„ฑ +- [ ] ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ์™„๋ฃŒ (100๋ช…) + +--- + +## 8. ๋‹ค์Œ ๋‹จ๊ณ„ (Phase 2 ์ดํ›„) + +**MVP ์ดํ›„ ํ™•์žฅ ๊ณ„ํš** (์„ ํƒ ์‚ฌํ•ญ): + +- **Retry ํŒจํ„ด**: ์ผ์‹œ์  ์˜ค๋ฅ˜ ์ž๋™ ์žฌ์‹œ๋„ (ํ˜„์žฌ๋Š” Circuit Breaker๋กœ ์ปค๋ฒ„) +- **Queue-Based Load Leveling**: ํŠธ๋ž˜ํ”ฝ ํญ์ฆ ์‹œ ๋ถ€ํ•˜ ๋ถ„์‚ฐ +- **Saga ํŒจํ„ด**: ๋ณต์žกํ•œ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ (์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ) +- **CQRS**: ์ฝ๊ธฐ/์“ฐ๊ธฐ ๋ถ„๋ฆฌ๋กœ ๋Œ€์‹œ๋ณด๋“œ ์„ฑ๋Šฅ ์ตœ์ ํ™” +- **Event Sourcing**: ์ด๋ฒคํŠธ ๋ณ€๊ฒฝ ์ด๋ ฅ ์ถ”์  ๋ฐ ๊ฐ์‚ฌ + +--- + +## ์ฐธ๊ณ  ๋ฌธ์„œ + +- [์œ ์ €์Šคํ† ๋ฆฌ](../userstory.md) +- [UI/UX ์„ค๊ณ„์„œ](../uiux/uiux.md) +- [ํด๋ผ์šฐ๋“œ ๋””์ž์ธ ํŒจํ„ด ๊ฐœ์š”](../../claude/cloud-design-patterns.md) +- [๋ฐฑ์—… ํŒŒ์ผ](./architecture-pattern-backup.md) - ์ด์ „ ๋ฒ„์ „ (9๊ฐœ ํŒจํ„ด) diff --git a/design/pattern/backup/architecture-pattern-backup.md b/design/pattern/backup/architecture-pattern-backup.md new file mode 100644 index 0000000..46f9f30 --- /dev/null +++ b/design/pattern/backup/architecture-pattern-backup.md @@ -0,0 +1,1097 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ํด๋ผ์šฐ๋“œ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด ์ ์šฉ ๋ฐฉ์•ˆ + +**๋ฌธ์„œ ์ •๋ณด** +- ํ”„๋กœ์ ํŠธ๋ช…: KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค +- ์ž‘์„ฑ์ผ: 2025-10-21 +- ๋ฒ„์ „: 1.0 +- ์ž‘์„ฑ์ž: System Architect + +--- + +## ๋ชฉ์ฐจ +1. [์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„ ๊ฒฐ๊ณผ](#1-์š”๊ตฌ์‚ฌํ•ญ-๋ถ„์„-๊ฒฐ๊ณผ) +2. [ํŒจํ„ด ํ‰๊ฐ€ ๋งคํŠธ๋ฆญ์Šค](#2-ํŒจํ„ด-ํ‰๊ฐ€-๋งคํŠธ๋ฆญ์Šค) +3. [์„œ๋น„์Šค๋ณ„ ํŒจํ„ด ์ ์šฉ ์„ค๊ณ„](#3-์„œ๋น„์Šค๋ณ„-ํŒจํ„ด-์ ์šฉ-์„ค๊ณ„) +4. [Phase๋ณ„ ๊ตฌํ˜„ ๋กœ๋“œ๋งต](#4-phase๋ณ„-๊ตฌํ˜„-๋กœ๋“œ๋งต) +5. [์˜ˆ์ƒ ์„ฑ๊ณผ ์ง€ํ‘œ](#5-์˜ˆ์ƒ-์„ฑ๊ณผ-์ง€ํ‘œ) + +--- + +## 1. ์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„ ๊ฒฐ๊ณผ + +### 1.1 ๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ + +#### ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋ณ„ ํ•ต์‹ฌ ๊ธฐ๋Šฅ +1. **User ์„œ๋น„์Šค** (4๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ) + - ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ/ํ”„๋กœํ•„ ๊ด€๋ฆฌ + - ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ (๊ตญ์„ธ์ฒญ API ์—ฐ๋™) + - ์„ธ์…˜ ๊ด€๋ฆฌ ๋ฐ ์ธ์ฆ/์ธ๊ฐ€ + +2. **Event ์„œ๋น„์Šค** (7๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ) + - ์ด๋ฒคํŠธ CRUD ๊ด€๋ฆฌ + - ๋Œ€์‹œ๋ณด๋“œ ํ˜„ํ™ฉ ์กฐํšŒ + - ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ ๋ฐ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ ๊ด€๋ฆฌ + - AI/Content/Distribution ์„œ๋น„์Šค ์—ฐ๋™ + +3. **AI ์„œ๋น„์Šค** (1๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ) + - ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ ๋ถ„์„ (5์ดˆ ์ด๋‚ด) + - 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ์ถ”์ฒœ ์ƒ์„ฑ (5์ดˆ ์ด๋‚ด) + - Claude API/GPT-4 API ์—ฐ๋™ + - **๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ**: ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ด๋ฒคํŠธ ์ถ”์ฒœ ๋™์‹œ ์‹คํ–‰ (์ด 10์ดˆ ๋ชฉํ‘œ) + +4. **Content ์„œ๋น„์Šค** (2๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ) + - SNS ์ด๋ฏธ์ง€ 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์ž๋™ ์ƒ์„ฑ (5์ดˆ ์ด๋‚ด) + - ํ”Œ๋žซํผ๋ณ„ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” (Instagram/Naver/Kakao) + - Stable Diffusion/DALL-E API ์—ฐ๋™ + +5. **Distribution ์„œ๋น„์Šค** (2๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ) + - ๋‹ค์ค‘ ์ฑ„๋„ ๋™์‹œ ๋ฐฐํฌ (์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS) + - ์ฑ„๋„๋ณ„ ๋…๋ฆฝ ์ฒ˜๋ฆฌ ๋ฐ ์‹คํŒจ ๋ณต๊ตฌ + - ๋ฐฐํฌ ์ƒํƒœ ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง + +6. **Participation ์„œ๋น„์Šค** (3๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ) + - ์ด๋ฒคํŠธ ์ฐธ์—ฌ์ž ๊ด€๋ฆฌ (์ค‘๋ณต ์ฒดํฌ) + - ์ž๋™ ์ถ”์ฒจ ์‹œ์Šคํ…œ + - ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ ๋ฐ ํ•„ํ„ฐ๋ง + +7. **Analytics ์„œ๋น„์Šค** (1๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ) + - ์‹ค์‹œ๊ฐ„ ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ (5๋ถ„ ๊ฐ„๊ฒฉ ์—…๋ฐ์ดํŠธ) + - ๋‹ค์ค‘ ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ†ตํ•ฉ (Participation, Distribution, POS, ์™ธ๋ถ€ API) + - ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ ๋ฐ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  ๊ณ„์‚ฐ + +### 1.2 ๋น„๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ + +#### ์„ฑ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ +| ํ•ญ๋ชฉ | ๋ชฉํ‘œ | ์šฐ์„ ์ˆœ์œ„ | +|------|------|---------| +| AI ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ถ”์ฒœ | 10์ดˆ ์ด๋‚ด | ๐Ÿ”ด Critical | +| SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ | 5์ดˆ ์ด๋‚ด | ๐Ÿ”ด Critical | +| ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ | 1๋ถ„ ์ด๋‚ด | ๐ŸŸก High | +| ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ์—…๋ฐ์ดํŠธ | 5๋ถ„ ๊ฐ„๊ฒฉ | ๐ŸŸข Medium | +| API ์‘๋‹ต ์‹œ๊ฐ„ (CRUD) | 200ms ์ดํ•˜ | ๐ŸŸก High | + +#### ๊ฐ€์šฉ์„ฑ ๋ฐ ์‹ ๋ขฐ์„ฑ +- **์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ**: 99.9% (์›” 43๋ถ„ ๋‹ค์šดํƒ€์ž„ ํ—ˆ์šฉ) +- **์™ธ๋ถ€ API ์žฅ์•  ๋Œ€์‘**: ๊ฐœ๋ณ„ ์ฑ„๋„ ์‹คํŒจ๊ฐ€ ์ „์ฒด ์„œ๋น„์Šค์— ์˜ํ–ฅ ์—†๋„๋ก ๊ฒฉ๋ฆฌ +- **๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ**: ์ด๋ฒคํŠธ ์ƒ์„ฑ ํŠธ๋žœ์žญ์…˜ ๋ณด์žฅ + +#### ํ™•์žฅ์„ฑ +- **์‚ฌ์šฉ์ž ์ฆ๊ฐ€ ๋Œ€์‘**: ์ดˆ๊ธฐ 100๋ช… โ†’ 1๋…„ ํ›„ 10,000๋ช… +- **์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋Ÿ‰**: ๋™์‹œ ์ด๋ฒคํŠธ ์ƒ์„ฑ 50๊ฐœ +- **๋ฐ์ดํ„ฐ ์ฆ๊ฐ€**: ์ผ ํ‰๊ท  1,000๊ฐœ ์ฐธ์—ฌ ๋ฐ์ดํ„ฐ + +#### ๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ +- JWT ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ +- API Gateway๋ฅผ ํ†ตํ•œ ์ค‘์•™ ์ธ์ฆ/์ธ๊ฐ€ +- ๊ฐœ์ธ์ •๋ณด ์•”ํ˜ธํ™” ์ €์žฅ (์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฆ„) +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ๊ฐ•ํ™” + +### 1.3 UI/UX ๋ถ„์„์—์„œ ๋„์ถœ๋œ ๊ธฐ์ˆ ์  ์š”๊ตฌ์‚ฌํ•ญ + +#### ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ํŒจํ„ด +- **Mobile First**: 60% ๋ชจ๋ฐ”์ผ, 40% ๋ฐ์Šคํฌํ†ฑ +- **์งง์€ ์„ธ์…˜**: 5-10๋ถ„ ๋‚ด ์ด๋ฒคํŠธ ์ƒ์„ฑ ์™„๋ฃŒ +- **์‹ค์‹œ๊ฐ„ ํ”ผ๋“œ๋ฐฑ**: ๋กœ๋”ฉ ์ƒํƒœ, ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ ํ•„์ˆ˜ +- **3 Tap Rule**: ๋ชจ๋“  ์ฃผ์š” ๊ธฐ๋Šฅ 3๋ฒˆ ํƒญ ๋‚ด ๋„๋‹ฌ + +#### ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ ์š”๊ตฌ์‚ฌํ•ญ +- AI ์ฒ˜๋ฆฌ ์ง„ํ–‰ ์ƒํ™ฉ ์‹ค์‹œ๊ฐ„ ํ‘œ์‹œ +- ๋ฐฐํฌ ์ƒํƒœ ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง +- ๋Œ€์‹œ๋ณด๋“œ ์ž๋™ ๊ฐฑ์‹  (5๋ถ„ ๊ฐ„๊ฒฉ) + +### 1.4 ๊ธฐ์ˆ ์  ๋„์ „๊ณผ์ œ + +#### 1. AI ์‘๋‹ต ์‹œ๊ฐ„ ๊ด€๋ฆฌ (๐Ÿ”ด Critical) +**๋ฌธ์ œ**: AI API ์‘๋‹ต์ด 10์ดˆ ๋ชฉํ‘œ๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์žˆ์Œ + +**์˜ํ–ฅ**: +- ์‚ฌ์šฉ์ž ์ดํƒˆ ์ฆ๊ฐ€ +- ์„œ๋น„์Šค ๋งŒ์กฑ๋„ ์ €ํ•˜ + +**ํ•ด๊ฒฐ ํ•„์š”์„ฑ**: MVP ๋‹จ๊ณ„๋ถ€ํ„ฐ ํ•„์ˆ˜ + +#### 2. ๋‹ค์ค‘ ์™ธ๋ถ€ API ์˜์กด์„ฑ (๐Ÿ”ด Critical) +**๋ฌธ์ œ**: 7๊ฐœ ์™ธ๋ถ€ API ์—ฐ๋™ (๊ตญ์„ธ์ฒญ, Claude/GPT-4, Stable Diffusion, ์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS) + +**์˜ํ–ฅ**: +- API ์žฅ์•  ์‹œ ์„œ๋น„์Šค ์ค‘๋‹จ ์œ„ํ—˜ +- ์‘๋‹ต ์‹œ๊ฐ„ ๋ถˆ์•ˆ์ •์„ฑ +- ๋น„์šฉ ์ฆ๊ฐ€ (API ํ˜ธ์ถœ๋Ÿ‰) + +**ํ•ด๊ฒฐ ํ•„์š”์„ฑ**: MVP ๋‹จ๊ณ„๋ถ€ํ„ฐ ํ•„์ˆ˜ + +#### 3. ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ์„ฑ๋Šฅ (๐ŸŸก High) +**๋ฌธ์ œ**: ๋‹ค์ค‘ ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ†ตํ•ฉ (Participation, Distribution, POS, ์™ธ๋ถ€ API) + +**์˜ํ–ฅ**: +- ๋Œ€์‹œ๋ณด๋“œ ๋กœ๋”ฉ ์ง€์—ฐ +- ์„œ๋ฒ„ ๋ถ€ํ•˜ ์ฆ๊ฐ€ + +**ํ•ด๊ฒฐ ํ•„์š”์„ฑ**: Phase 2 ํ™•์žฅ ๋‹จ๊ณ„์—์„œ ํ•ด๊ฒฐ + +#### 4. ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ํŠธ๋žœ์žญ์…˜ (๐ŸŸก High) +**๋ฌธ์ œ**: ์ด๋ฒคํŠธ ์ƒ์„ฑ โ†’ AI ์ถ”์ฒœ โ†’ ์ฝ˜ํ…์ธ  ์ƒ์„ฑ โ†’ ๋ฐฐํฌ โ†’ ์ฐธ์—ฌ์ž ๊ด€๋ฆฌ โ†’ ๋ถ„์„ + +**์˜ํ–ฅ**: +- ๋ถ€๋ถ„ ์‹คํŒจ ์‹œ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ +- ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ๋ณต์žก๋„ ์ฆ๊ฐ€ + +**ํ•ด๊ฒฐ ํ•„์š”์„ฑ**: Phase 1 MVP์—์„œ ๊ธฐ๋ณธ ๊ตฌ์กฐ ํ™•๋ฆฝ + +--- + +## 2. ํŒจํ„ด ํ‰๊ฐ€ ๋งคํŠธ๋ฆญ์Šค + +### 2.1 ํ‰๊ฐ€ ๊ธฐ์ค€ + +| ๊ธฐ์ค€ | ๊ฐ€์ค‘์น˜ | ํ‰๊ฐ€ ๋‚ด์šฉ | +|------|--------|-----------| +| **๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ** | 35% | ์š”๊ตฌ์‚ฌํ•ญ์„ ์ง์ ‘ ํ•ด๊ฒฐํ•˜๋Š” ๋Šฅ๋ ฅ | +| **์„ฑ๋Šฅ ํšจ๊ณผ** | 25% | ์‘๋‹ต์‹œ๊ฐ„ ๋ฐ ์ฒ˜๋ฆฌ๋Ÿ‰ ๊ฐœ์„  ํšจ๊ณผ | +| **์šด์˜ ๋ณต์žก๋„** | 20% | ๊ตฌํ˜„ ๋ฐ ์šด์˜์˜ ์šฉ์ด์„ฑ | +| **ํ™•์žฅ์„ฑ** | 15% | ๋ฏธ๋ž˜ ์š”๊ตฌ์‚ฌํ•ญ์— ๋Œ€ํ•œ ๋Œ€์‘๋ ฅ | +| **๋น„์šฉ ํšจ์œจ์„ฑ** | 5% | ๊ฐœ๋ฐœ/์šด์˜ ๋น„์šฉ ๋Œ€๋น„ ํšจ๊ณผ(ROI) | + +### 2.2 ํ•ต์‹ฌ ํŒจํ„ด ํ‰๊ฐ€ + +#### 2.2.1 API Gateway (ํ•„์ˆ˜ ํŒจํ„ด) + +| ํ‰๊ฐ€ ๊ธฐ์ค€ | ์ ์ˆ˜ | ๊ฐ€์ค‘์น˜ | ๊ณ„์‚ฐ | ๊ทผ๊ฑฐ | +|----------|-----|--------|------|------| +| ๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ | 9 | 35% | 3.15 | ์ค‘์•™ ์ธ์ฆ, ๋ผ์šฐํŒ…, ๋กœ๊น… ํ†ตํ•ฉ ์ œ๊ณต | +| ์„ฑ๋Šฅ ํšจ๊ณผ | 7 | 25% | 1.75 | ๋‹จ์ผ ์—”๋“œํฌ์ธํŠธ๋กœ ๋„คํŠธ์›Œํฌ ์ตœ์ ํ™” | +| ์šด์˜ ๋ณต์žก๋„ | 8 | 20% | 1.60 | ํ‘œ์ค€ ์†”๋ฃจ์…˜ ํ™œ์šฉ (Kong, AWS API Gateway) | +| ํ™•์žฅ์„ฑ | 9 | 15% | 1.35 | ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์ฆ๊ฐ€์— ์œ ์—ฐ ๋Œ€์‘ | +| ๋น„์šฉ ํšจ์œจ์„ฑ | 8 | 5% | 0.40 | ์˜คํ”ˆ์†Œ์Šค(Kong) ๋˜๋Š” ํด๋ผ์šฐ๋“œ ์„œ๋น„์Šค ํ™œ์šฉ | +| **์ด์ ** | | | **8.25** | **์„ ์ •** | + +**์„ ์ • ์ด์œ **: +- 7๊ฐœ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์˜ ๋‹จ์ผ ์ง„์ž…์  ํ•„์š” +- ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ(์ธ์ฆ, ๋กœ๊น…, Rate Limiting) ์ค‘์•™ ๊ด€๋ฆฌ +- Mobile First ํ™˜๊ฒฝ์—์„œ ๋‹จ์ผ ์—”๋“œํฌ์ธํŠธ ํ•„์ˆ˜ + +**์ ์šฉ ๋ฐฉ์•ˆ**: +- Kong Gateway ๋˜๋Š” AWS API Gateway ํ™œ์šฉ +- JWT ํ† ํฐ ๊ฒ€์ฆ ์ค‘์•™ํ™” +- Rate Limiting ์ ์šฉ (DoS ๋ฐฉ์ง€) +- Request/Response ๋กœ๊น… + +--- + +#### 2.2.2 Cache-Aside (ํ•„์ˆ˜ ํŒจํ„ด) + +| ํ‰๊ฐ€ ๊ธฐ์ค€ | ์ ์ˆ˜ | ๊ฐ€์ค‘์น˜ | ๊ณ„์‚ฐ | ๊ทผ๊ฑฐ | +|----------|-----|--------|------|------| +| ๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ | 9 | 35% | 3.15 | AI ํŠธ๋ Œ๋“œ ๋ถ„์„, ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๊ฒฐ๊ณผ ์บ์‹ฑ | +| ์„ฑ๋Šฅ ํšจ๊ณผ | 10 | 25% | 2.50 | AI API ํ˜ธ์ถœ ๋Œ€ํญ ๊ฐ์†Œ (10์ดˆ โ†’ 0.1์ดˆ) | +| ์šด์˜ ๋ณต์žก๋„ | 9 | 20% | 1.80 | Redis ํ‘œ์ค€ ํŒจํ„ด, ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ | +| ํ™•์žฅ์„ฑ | 8 | 15% | 1.20 | Redis Cluster๋กœ ํ™•์žฅ ๊ฐ€๋Šฅ | +| ๋น„์šฉ ํšจ์œจ์„ฑ | 10 | 5% | 0.50 | AI API ๋น„์šฉ 90% ์ ˆ๊ฐ | +| **์ด์ ** | | | **9.15** | **์„ ์ •** | + +**์„ ์ • ์ด์œ **: +- AI API ๋น„์šฉ ์ ˆ๊ฐ (๋™์ผํ•œ ์š”์ฒญ์— ๋Œ€ํ•œ ๋ฐ˜๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€) +- ์„ฑ๋Šฅ ๋Œ€ํญ ๊ฐœ์„  (10์ดˆ โ†’ ์บ์‹œ ํžˆํŠธ ์‹œ 0.1์ดˆ) +- ์™ธ๋ถ€ API ์˜์กด์„ฑ ๊ฐ์†Œ + +**์ ์šฉ ๋Œ€์ƒ**: +1. **AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ**: ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ๋ณ„ 1์‹œ๊ฐ„ ์บ์‹ฑ +2. **AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ๊ฒฐ๊ณผ**: ๋™์ผ ์กฐ๊ฑด 24์‹œ๊ฐ„ ์บ์‹ฑ +3. **SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๊ฒฐ๊ณผ**: ์˜๊ตฌ ์บ์‹ฑ (CDN ์—ฐ๋™) +4. **์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ๊ฒฐ๊ณผ**: 30์ผ ์บ์‹ฑ + +--- + +#### 2.2.3 Asynchronous Request-Reply (ํ•„์ˆ˜ ํŒจํ„ด) + +| ํ‰๊ฐ€ ๊ธฐ์ค€ | ์ ์ˆ˜ | ๊ฐ€์ค‘์น˜ | ๊ณ„์‚ฐ | ๊ทผ๊ฑฐ | +|----------|-----|--------|------|------| +| ๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ | 10 | 35% | 3.50 | AI ์ฒ˜๋ฆฌ ๋น„๋™๊ธฐ ๋Œ€์‘ ํ•„์ˆ˜ | +| ์„ฑ๋Šฅ ํšจ๊ณผ | 9 | 25% | 2.25 | ์‚ฌ์šฉ์ž ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๋‹จ์ถ•, ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค ํšจ์œจํ™” | +| ์šด์˜ ๋ณต์žก๋„ | 6 | 20% | 1.20 | ํด๋ง/WebSocket ๊ตฌํ˜„ ํ•„์š” | +| ํ™•์žฅ์„ฑ | 9 | 15% | 1.35 | ์žฅ์‹œ๊ฐ„ ์ž‘์—… ์ฆ๊ฐ€ ์‹œ ๋Œ€์‘ ๊ฐ€๋Šฅ | +| ๋น„์šฉ ํšจ์œจ์„ฑ | 7 | 5% | 0.35 | ๊ฐœ๋ฐœ ๋น„์šฉ ์ฆ๊ฐ€ํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„  | +| **์ด์ ** | | | **8.65** | **์„ ์ •** | + +**์„ ์ • ์ด์œ **: +- AI ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ด๋ฒคํŠธ ์ถ”์ฒœ (10์ดˆ ์ด์ƒ ์†Œ์š”) +- SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ (5์ดˆ ์ด์ƒ ์†Œ์š”) +- ์‚ฌ์šฉ์ž ์ดํƒˆ ๋ฐฉ์ง€ (๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ + ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ) + +**์ ์šฉ ๋ฐฉ์•ˆ**: +1. **์š”์ฒญ ๋‹จ๊ณ„**: ํด๋ผ์ด์–ธํŠธ โ†’ API Gateway โ†’ AI ์„œ๋น„์Šค (Job ID ๋ฐ˜ํ™˜) +2. **์ฒ˜๋ฆฌ ๋‹จ๊ณ„**: AI ์„œ๋น„์Šค ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ, ์ง„ํ–‰ ์ƒํ™ฉ Redis ์ €์žฅ +3. **์‘๋‹ต ๋‹จ๊ณ„**: ํด๋ผ์ด์–ธํŠธ ํด๋ง ๋˜๋Š” WebSocket์œผ๋กœ ์ง„ํ–‰ ์ƒํ™ฉ ํ™•์ธ +4. **์™„๋ฃŒ ๋‹จ๊ณ„**: ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ๋ฐ ์บ์‹ฑ + +--- + +#### 2.2.4 Circuit Breaker (ํ•„์ˆ˜ ํŒจํ„ด) + +| ํ‰๊ฐ€ ๊ธฐ์ค€ | ์ ์ˆ˜ | ๊ฐ€์ค‘์น˜ | ๊ณ„์‚ฐ | ๊ทผ๊ฑฐ | +|----------|-----|--------|------|------| +| ๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ | 10 | 35% | 3.50 | ์™ธ๋ถ€ API ์žฅ์•  ๊ฒฉ๋ฆฌ ํ•„์ˆ˜ | +| ์„ฑ๋Šฅ ํšจ๊ณผ | 8 | 25% | 2.00 | ์žฅ์•  ์ „ํŒŒ ๋ฐฉ์ง€๋กœ ์ „์ฒด ์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ ์œ ์ง€ | +| ์šด์˜ ๋ณต์žก๋„ | 7 | 20% | 1.40 | ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ™œ์šฉ (Resilience4j, Netflix Hystrix) | +| ํ™•์žฅ์„ฑ | 9 | 15% | 1.35 | ์™ธ๋ถ€ API ์ฆ๊ฐ€ ์‹œ ๋™์ผ ํŒจํ„ด ์ ์šฉ | +| ๋น„์šฉ ํšจ์œจ์„ฑ | 8 | 5% | 0.40 | ์žฅ์•  ์‹œ ์ „์ฒด ์‹œ์Šคํ…œ ๋‹ค์šด ๋ฐฉ์ง€๋กœ ์†์‹ค ๊ฐ์†Œ | +| **์ด์ ** | | | **8.65** | **์„ ์ •** | + +**์„ ์ • ์ด์œ **: +- 7๊ฐœ ์™ธ๋ถ€ API ์—ฐ๋™ (Claude/GPT-4, Stable Diffusion, ์šฐ๋ฆฌ๋™๋„คTV ๋“ฑ) +- API ์žฅ์•  ์‹œ ์ „์ฒด ์„œ๋น„์Šค ์ค‘๋‹จ ๋ฐฉ์ง€ +- ๋น ๋ฅธ ์‹คํŒจ ๋ฐ ํด๋ฐฑ ์ฒ˜๋ฆฌ + +**์ ์šฉ ๋Œ€์ƒ**: +1. **AI API**: Claude/GPT-4 ์žฅ์•  ์‹œ ๋ฏธ๋ฆฌ ์ค€๋น„๋œ ํ…œํ”Œ๋ฆฟ ๋ฐ˜ํ™˜ +2. **์ด๋ฏธ์ง€ ์ƒ์„ฑ API**: Stable Diffusion ์žฅ์•  ์‹œ ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์ด๋ฏธ์ง€ ์ œ๊ณต +3. **๋ฐฐํฌ ์ฑ„๋„ API**: ๊ฐœ๋ณ„ ์ฑ„๋„ ์žฅ์• ๊ฐ€ ๋‹ค๋ฅธ ์ฑ„๋„์— ์˜ํ–ฅ ์—†๋„๋ก ๊ฒฉ๋ฆฌ +4. **๊ตญ์„ธ์ฒญ API**: ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ์บ์‹œ๋œ ๊ฒฐ๊ณผ ๋˜๋Š” ์ˆ˜๋™ ์Šน์ธ ํ”Œ๋กœ์šฐ + +--- + +#### 2.2.5 Retry (ํ•„์ˆ˜ ํŒจํ„ด) + +| ํ‰๊ฐ€ ๊ธฐ์ค€ | ์ ์ˆ˜ | ๊ฐ€์ค‘์น˜ | ๊ณ„์‚ฐ | ๊ทผ๊ฑฐ | +|----------|-----|--------|------|------| +| ๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ | 9 | 35% | 3.15 | ์ผ์‹œ์  ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜ ๋Œ€์‘ | +| ์„ฑ๋Šฅ ํšจ๊ณผ | 7 | 25% | 1.75 | ์ผ์‹œ์  ์˜ค๋ฅ˜ ๋ณต๊ตฌ๋กœ ์‚ฌ์šฉ์ž ์žฌ์‹œ๋„ ๋ถˆํ•„์š” | +| ์šด์˜ ๋ณต์žก๋„ | 9 | 20% | 1.80 | ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ™œ์šฉ (Resilience4j) | +| ํ™•์žฅ์„ฑ | 8 | 15% | 1.20 | ๋ชจ๋“  ์™ธ๋ถ€ API์— ๋™์ผ ์ ์šฉ | +| ๋น„์šฉ ํšจ์œจ์„ฑ | 7 | 5% | 0.35 | ์žฌ์‹œ๋„ ๋น„์šฉ < ์‹คํŒจ ์ฒ˜๋ฆฌ ๋น„์šฉ | +| **์ด์ ** | | | **8.25** | **์„ ์ •** | + +**์„ ์ • ์ด์œ **: +- ์™ธ๋ถ€ API ์ผ์‹œ์  ์˜ค๋ฅ˜ ๋นˆ๋ฒˆ (๋„คํŠธ์›Œํฌ ํƒ€์ž„์•„์›ƒ ๋“ฑ) +- ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„  (์ž๋™ ๋ณต๊ตฌ) + +**์ ์šฉ ๋ฐฉ์•ˆ**: +- **Exponential Backoff**: 1์ดˆ โ†’ 2์ดˆ โ†’ 4์ดˆ +- **์ตœ๋Œ€ ์žฌ์‹œ๋„**: 3ํšŒ +- **์ ์šฉ ๋Œ€์ƒ**: ๋ชจ๋“  ์™ธ๋ถ€ API ํ˜ธ์ถœ +- **Circuit Breaker ์—ฐ๋™**: ์žฌ์‹œ๋„ ์‹คํŒจ ์‹œ Circuit ์˜คํ”ˆ + +--- + +#### 2.2.6 Queue-Based Load Leveling (ํ™•์žฅ ๋‹จ๊ณ„ ํŒจํ„ด) + +| ํ‰๊ฐ€ ๊ธฐ์ค€ | ์ ์ˆ˜ | ๊ฐ€์ค‘์น˜ | ๊ณ„์‚ฐ | ๊ทผ๊ฑฐ | +|----------|-----|--------|------|------| +| ๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ | 7 | 35% | 2.45 | ๋Œ€๋Ÿ‰ ์ด๋ฒคํŠธ ์ƒ์„ฑ ์‹œ ๋ถ€ํ•˜ ๋ถ„์‚ฐ | +| ์„ฑ๋Šฅ ํšจ๊ณผ | 8 | 25% | 2.00 | ํ”ผํฌ ์‹œ๊ฐ„๋Œ€ ์„œ๋ฒ„ ์•ˆ์ •์„ฑ ํ™•๋ณด | +| ์šด์˜ ๋ณต์žก๋„ | 5 | 20% | 1.00 | ๋ฉ”์‹œ์ง€ ํ ์ธํ”„๋ผ ํ•„์š” (RabbitMQ/Kafka) | +| ํ™•์žฅ์„ฑ | 9 | 15% | 1.35 | ํŠธ๋ž˜ํ”ฝ ์ฆ๊ฐ€ ์‹œ ์ˆ˜ํ‰ ํ™•์žฅ ์šฉ์ด | +| ๋น„์šฉ ํšจ์œจ์„ฑ | 6 | 5% | 0.30 | ์ธํ”„๋ผ ์ถ”๊ฐ€ ๋น„์šฉ ๋ฐœ์ƒ | +| **์ด์ ** | | | **7.10** | **Phase 2 ์„ ์ •** | + +**์„ ์ • ์ด์œ **: +- MVP์—์„œ๋Š” ๋ถˆํ•„์š” (์‚ฌ์šฉ์ž 100๋ช… ์ดํ•˜) +- Phase 2 ํ™•์žฅ ๋‹จ๊ณ„์—์„œ ์‚ฌ์šฉ์ž ์ฆ๊ฐ€ ์‹œ ํ•„์š” + +**์ ์šฉ ์‹œ๊ธฐ**: Phase 2 (์‚ฌ์šฉ์ž 1,000๋ช… ์ด์ƒ) + +**์ ์šฉ ๋Œ€์ƒ**: +- AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ์š”์ฒญ ํ +- SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ ํ +- ๋ฐฐํฌ ์š”์ฒญ ํ + +--- + +#### 2.2.7 CQRS (๊ณ ๋„ํ™” ๋‹จ๊ณ„ ํŒจํ„ด) + +| ํ‰๊ฐ€ ๊ธฐ์ค€ | ์ ์ˆ˜ | ๊ฐ€์ค‘์น˜ | ๊ณ„์‚ฐ | ๊ทผ๊ฑฐ | +|----------|-----|--------|------|------| +| ๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ | 8 | 35% | 2.80 | Analytics ๋Œ€์‹œ๋ณด๋“œ ์ฝ๊ธฐ ์ตœ์ ํ™” | +| ์„ฑ๋Šฅ ํšจ๊ณผ | 9 | 25% | 2.25 | ์ฝ๊ธฐ ์ „์šฉ DB๋กœ ๋Œ€์‹œ๋ณด๋“œ ์„ฑ๋Šฅ ๋Œ€ํญ ๊ฐœ์„  | +| ์šด์˜ ๋ณต์žก๋„ | 4 | 20% | 0.80 | ์ฝ๊ธฐ/์“ฐ๊ธฐ DB ๋ถ„๋ฆฌ, ๋™๊ธฐํ™” ๋ณต์žก๋„ ์ฆ๊ฐ€ | +| ํ™•์žฅ์„ฑ | 10 | 15% | 1.50 | ์ฝ๊ธฐ/์“ฐ๊ธฐ ๋…๋ฆฝ ํ™•์žฅ ๊ฐ€๋Šฅ | +| ๋น„์šฉ ํšจ์œจ์„ฑ | 5 | 5% | 0.25 | DB ์ธํ”„๋ผ ๋น„์šฉ ์ฆ๊ฐ€ | +| **์ด์ ** | | | **7.60** | **Phase 3 ์„ ์ •** | + +**์„ ์ • ์ด์œ **: +- MVP์—์„œ๋Š” ๊ณผ๋„ํ•œ ๋ณต์žก๋„ +- Phase 3 ๊ณ ๋„ํ™” ๋‹จ๊ณ„์—์„œ ๋Œ€์‹œ๋ณด๋“œ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์‹œ ์ ์šฉ + +**์ ์šฉ ์‹œ๊ธฐ**: Phase 3 (์‚ฌ์šฉ์ž 5,000๋ช… ์ด์ƒ) + +**์ ์šฉ ๋Œ€์ƒ**: +- Analytics ์„œ๋น„์Šค (์ฝ๊ธฐ ์ „์šฉ DB) +- ๋ณต์žกํ•œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ์ตœ์ ํ™” + +--- + +#### 2.2.8 Saga (ํ™•์žฅ ๋‹จ๊ณ„ ํŒจํ„ด) + +| ํ‰๊ฐ€ ๊ธฐ์ค€ | ์ ์ˆ˜ | ๊ฐ€์ค‘์น˜ | ๊ณ„์‚ฐ | ๊ทผ๊ฑฐ | +|----------|-----|--------|------|------| +| ๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ | 8 | 35% | 2.80 | ์ด๋ฒคํŠธ ์ƒ์„ฑ โ†’ AI โ†’ ์ฝ˜ํ…์ธ  โ†’ ๋ฐฐํฌ ํŠธ๋žœ์žญ์…˜ | +| ์„ฑ๋Šฅ ํšจ๊ณผ | 6 | 25% | 1.50 | ์„ฑ๋Šฅ ๊ฐœ์„  ํšจ๊ณผ ๋ฏธ๋ฏธ | +| ์šด์˜ ๋ณต์žก๋„ | 3 | 20% | 0.60 | ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์„ค๊ณ„ ๋ณต์žก๋„ ๋งค์šฐ ๋†’์Œ | +| ํ™•์žฅ์„ฑ | 9 | 15% | 1.35 | ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ํ”Œ๋กœ์šฐ ์ฆ๊ฐ€ ์‹œ ํ•„์ˆ˜ | +| ๋น„์šฉ ํšจ์œจ์„ฑ | 4 | 5% | 0.20 | ๊ฐœ๋ฐœ ๋น„์šฉ ์ฆ๊ฐ€ | +| **์ด์ ** | | | **6.45** | **Phase 2 ์„ ์ •** | + +**์„ ์ • ์ด์œ **: +- MVP์—์„œ๋Š” ๋‹จ์ˆœ ๋กค๋ฐฑ ์ฒ˜๋ฆฌ๋กœ ์ถฉ๋ถ„ +- Phase 2์—์„œ ๋ณต์žกํ•œ ํ”Œ๋กœ์šฐ ์ฆ๊ฐ€ ์‹œ ํ•„์š” + +**์ ์šฉ ์‹œ๊ธฐ**: Phase 2 + +**์ ์šฉ ๋Œ€์ƒ**: +- ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ (Event โ†’ AI โ†’ Content โ†’ Distribution) +- ๊ฒฐ์ œ ์—ฐ๋™ ์‹œ (Phase 3) + +--- + +#### 2.2.9 Event Sourcing (๊ณ ๋„ํ™” ๋‹จ๊ณ„ ํŒจํ„ด) + +| ํ‰๊ฐ€ ๊ธฐ์ค€ | ์ ์ˆ˜ | ๊ฐ€์ค‘์น˜ | ๊ณ„์‚ฐ | ๊ทผ๊ฑฐ | +|----------|-----|--------|------|------| +| ๊ธฐ๋Šฅ ์ ํ•ฉ์„ฑ | 7 | 35% | 2.45 | ์ด๋ฒคํŠธ ๋ณ€๊ฒฝ ์ด๋ ฅ ์ถ”์  | +| ์„ฑ๋Šฅ ํšจ๊ณผ | 5 | 25% | 1.25 | ์„ฑ๋Šฅ ๊ฐœ์„  ํšจ๊ณผ ๋ฏธ๋ฏธ | +| ์šด์˜ ๋ณต์žก๋„ | 3 | 20% | 0.60 | ์ด๋ฒคํŠธ ์ €์žฅ์†Œ ๊ด€๋ฆฌ, ์ด๋ฒคํŠธ ์žฌ์ƒ ๋ณต์žก๋„ ๋งค์šฐ ๋†’์Œ | +| ํ™•์žฅ์„ฑ | 8 | 15% | 1.20 | ๊ฐ์‚ฌ ์ถ”์ , ๋””๋ฒ„๊น…์— ์œ ์šฉ | +| ๋น„์šฉ ํšจ์œจ์„ฑ | 4 | 5% | 0.20 | ์Šคํ† ๋ฆฌ์ง€ ๋น„์šฉ ์ฆ๊ฐ€ | +| **์ด์ ** | | | **5.70** | **Phase 3 ์„ ์ •** | + +**์„ ์ • ์ด์œ **: +- MVP์—์„œ๋Š” ๋ถˆํ•„์š” +- Phase 3์—์„œ ๊ฐ์‚ฌ ์ถ”์  ์š”๊ตฌ์‚ฌํ•ญ ๋ฐœ์ƒ ์‹œ ์ ์šฉ + +**์ ์šฉ ์‹œ๊ธฐ**: Phase 3 + +**์ ์šฉ ๋Œ€์ƒ**: +- Event ์„œ๋น„์Šค (์ด๋ฒคํŠธ ์ƒ์„ฑ/์ˆ˜์ •/์‚ญ์ œ ์ด๋ ฅ) + +--- + +### 2.3 ์„ ์ • ํŒจํ„ด ์š”์•ฝ + +#### Phase 1 (MVP) - ํ•„์ˆ˜ ํŒจํ„ด +| ํŒจํ„ด | ์ด์  | ์ ์šฉ ์„œ๋น„์Šค | ์šฐ์„ ์ˆœ์œ„ | +|------|------|------------|---------| +| Cache-Aside | 9.15 | AI, Content, User | ๐Ÿ”ด Critical | +| API Gateway | 8.25 | ์ „์ฒด | ๐Ÿ”ด Critical | +| Asynchronous Request-Reply | 8.65 | AI, Content | ๐Ÿ”ด Critical | +| Circuit Breaker | 8.65 | AI, Content, Distribution | ๐Ÿ”ด Critical | +| Retry | 8.25 | ์ „์ฒด (์™ธ๋ถ€ API) | ๐Ÿ”ด Critical | + +#### Phase 2 (ํ™•์žฅ) - ์ถ”๊ฐ€ ํŒจํ„ด +| ํŒจํ„ด | ์ด์  | ์ ์šฉ ์„œ๋น„์Šค | ์šฐ์„ ์ˆœ์œ„ | +|------|------|------------|---------| +| Queue-Based Load Leveling | 7.10 | AI, Content, Distribution | ๐ŸŸก High | +| Saga | 6.45 | Event, AI, Content, Distribution | ๐ŸŸก High | + +#### Phase 3 (๊ณ ๋„ํ™”) - ์ตœ์ ํ™” ํŒจํ„ด +| ํŒจํ„ด | ์ด์  | ์ ์šฉ ์„œ๋น„์Šค | ์šฐ์„ ์ˆœ์œ„ | +|------|------|------------|---------| +| CQRS | 7.60 | Analytics | ๐ŸŸข Medium | +| Event Sourcing | 5.70 | Event | ๐ŸŸข Medium | + +--- + +## 3. ์„œ๋น„์Šค๋ณ„ ํŒจํ„ด ์ ์šฉ ์„ค๊ณ„ + +### 3.1 ์ „์ฒด ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์กฐ (Phase 1 MVP) + +```mermaid +graph TB + subgraph "ํด๋ผ์ด์–ธํŠธ" + Mobile[๋ชจ๋ฐ”์ผ ์•ฑ] + Web[์›น ๋ธŒ๋ผ์šฐ์ €] + end + + subgraph "API Gateway ๋ ˆ์ด์–ด" + Gateway[API Gateway
- JWT ์ธ์ฆ
- Rate Limiting
- Logging] + end + + Mobile --> Gateway + Web --> Gateway + + subgraph "๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค" + User[User ์„œ๋น„์Šค
- ์ธ์ฆ/์ธ๊ฐ€
- ํ”„๋กœํ•„ ๊ด€๋ฆฌ] + Event[Event ์„œ๋น„์Šค
- ์ด๋ฒคํŠธ CRUD
- ํ”Œ๋กœ์šฐ ๊ด€๋ฆฌ] + AI[AI ์„œ๋น„์Šค
- ํŠธ๋ Œ๋“œ ๋ถ„์„
- ์ด๋ฒคํŠธ ์ถ”์ฒœ
โฑ๏ธ Async] + Content[Content ์„œ๋น„์Šค
- ์ด๋ฏธ์ง€ ์ƒ์„ฑ
โฑ๏ธ Async] + Distribution[Distribution ์„œ๋น„์Šค
- ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ] + Participation[Participation ์„œ๋น„์Šค
- ์ฐธ์—ฌ์ž ๊ด€๋ฆฌ
- ์ถ”์ฒจ] + Analytics[Analytics ์„œ๋น„์Šค
- ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ] + end + + Gateway --> User + Gateway --> Event + Gateway --> Participation + Gateway --> Analytics + + Event --> AI + Event --> Content + Event --> Distribution + + subgraph "์บ์‹œ ๋ ˆ์ด์–ด" + Redis[Redis Cache
๐Ÿ’พ Cache-Aside
- AI ๊ฒฐ๊ณผ
- ์ด๋ฏธ์ง€
- ์‚ฌ์—…์ž๋ฒˆํ˜ธ] + end + + AI -.->|์บ์‹œ ์กฐํšŒ/์ €์žฅ| Redis + Content -.->|์บ์‹œ ์กฐํšŒ/์ €์žฅ| Redis + User -.->|์บ์‹œ ์กฐํšŒ/์ €์žฅ| Redis + + subgraph "์™ธ๋ถ€ API (Circuit Breaker + Retry)" + Claude[Claude/GPT-4 API] + StableDiff[Stable Diffusion API] + NTS[๊ตญ์„ธ์ฒญ API] + UDTV[์šฐ๋ฆฌ๋™๋„คTV API] + RingoBiz[๋ง๊ณ ๋น„์ฆˆ API] + GenieTV[์ง€๋‹ˆTV API] + SNS[SNS APIs] + end + + AI -->|๐Ÿ”ด Circuit Breaker| Claude + Content -->|๐Ÿ”ด Circuit Breaker| StableDiff + User -->|๐Ÿ”ด Circuit Breaker| NTS + Distribution -->|๐Ÿ”ด Circuit Breaker| UDTV + Distribution -->|๐Ÿ”ด Circuit Breaker| RingoBiz + Distribution -->|๐Ÿ”ด Circuit Breaker| GenieTV + Distribution -->|๐Ÿ”ด Circuit Breaker| SNS + + subgraph "๋ฐ์ดํ„ฐ ๋ ˆ์ด์–ด" + UserDB[(User DB)] + EventDB[(Event DB)] + ParticipationDB[(Participation DB)] + end + + User --> UserDB + Event --> EventDB + Participation --> ParticipationDB + + Analytics -.->|๋ฐ์ดํ„ฐ ์ˆ˜์ง‘| ParticipationDB + Analytics -.->|๋ฐ์ดํ„ฐ ์ˆ˜์ง‘| EventDB + Analytics -.->|๋ฐ์ดํ„ฐ ์ˆ˜์ง‘| Distribution + + classDef asyncService fill:#ffe6e6,stroke:#ff4444,stroke-width:3px + classDef cacheLayer fill:#e6f3ff,stroke:#4444ff,stroke-width:3px + classDef gateway fill:#fff3e6,stroke:#ff8800,stroke-width:3px + + class AI,Content asyncService + class Redis cacheLayer + class Gateway gateway +``` + +### 3.2 ์„œ๋น„์Šค๋ณ„ ์ƒ์„ธ ํŒจํ„ด ์ ์šฉ + +#### 3.2.1 User ์„œ๋น„์Šค + +**์ ์šฉ ํŒจํ„ด**: +1. **Cache-Aside** (์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ๊ฒฐ๊ณผ) + - ๊ฒ€์ฆ ์™„๋ฃŒ๋œ ์‚ฌ์—…์ž๋ฒˆํ˜ธ 30์ผ ์บ์‹ฑ + - ์บ์‹œ ๋ฏธ์Šค ์‹œ ๊ตญ์„ธ์ฒญ API ํ˜ธ์ถœ + - ๊ตญ์„ธ์ฒญ API ๋น„์šฉ 90% ์ ˆ๊ฐ + +2. **Circuit Breaker** (๊ตญ์„ธ์ฒญ API) + - ์žฅ์•  ์‹œ ํด๋ฐฑ: ์บ์‹œ๋œ ๊ฒฐ๊ณผ ๋˜๋Š” ์ˆ˜๋™ ์Šน์ธ ํ”Œ๋กœ์šฐ + - Threshold: 10ํšŒ ์—ฐ์† ์‹คํŒจ + - Timeout: 30์ดˆ + +3. **Retry** (๊ตญ์„ธ์ฒญ API) + - ์žฌ์‹œ๋„: 3ํšŒ + - Backoff: 1์ดˆ โ†’ 2์ดˆ โ†’ 4์ดˆ + +**ํ•ต์‹ฌ ํ”Œ๋กœ์šฐ**: +``` +ํšŒ์›๊ฐ€์ž… ์š”์ฒญ + โ†’ ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ + โ†’ Redis ์บ์‹œ ์กฐํšŒ + โ†’ ์บ์‹œ ํžˆํŠธ: ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + โ†’ ์บ์‹œ ๋ฏธ์Šค: ๊ตญ์„ธ์ฒญ API ํ˜ธ์ถœ (Circuit Breaker + Retry) + โ†’ ์„ฑ๊ณต: Redis ์บ์‹ฑ (30์ผ) + โ†’ ์‹คํŒจ: ํด๋ฐฑ ์ฒ˜๋ฆฌ +``` + +--- + +#### 3.2.2 AI ์„œ๋น„์Šค (๐Ÿ”ด Critical) + +**์ ์šฉ ํŒจํ„ด**: +1. **Asynchronous Request-Reply** (ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ด๋ฒคํŠธ ์ถ”์ฒœ) + - ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ: 10์ดˆ ์ด์ƒ ์†Œ์š” + - Job ID ๋ฐ˜ํ™˜ โ†’ ํด๋ผ์ด์–ธํŠธ ํด๋ง + - ์ง„ํ–‰ ์ƒํ™ฉ Redis ์ €์žฅ + +2. **Cache-Aside** (ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ) + - ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ๋ณ„ 1์‹œ๊ฐ„ ์บ์‹ฑ + - ๋™์ผ ์กฐ๊ฑด ์š”์ฒญ ์‹œ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (10์ดˆ โ†’ 0.1์ดˆ) + +3. **Circuit Breaker** (Claude/GPT-4 API) + - ์žฅ์•  ์‹œ ํด๋ฐฑ: ๋ฏธ๋ฆฌ ์ค€๋น„๋œ ํ…œํ”Œ๋ฆฟ ๊ธฐ๋ฐ˜ ์ถ”์ฒœ + - Threshold: 5ํšŒ ์—ฐ์† ์‹คํŒจ + - Timeout: 15์ดˆ + +4. **Retry** (Claude/GPT-4 API) + - ์žฌ์‹œ๋„: 3ํšŒ + - Backoff: 2์ดˆ โ†’ 4์ดˆ โ†’ 8์ดˆ + +**ํ•ต์‹ฌ ํ”Œ๋กœ์šฐ**: +```mermaid +sequenceDiagram + participant Client + participant API_GW as API Gateway + participant Event + participant AI + participant Redis + participant Claude as Claude API + + Client->>API_GW: POST /events/ai-recommend + API_GW->>Event: ์ด๋ฒคํŠธ ์ถ”์ฒœ ์š”์ฒญ + Event->>AI: ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ถ”์ฒœ ์š”์ฒญ + + AI->>Redis: ์บ์‹œ ์กฐํšŒ (์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ) + + alt ์บ์‹œ ํžˆํŠธ + Redis-->>AI: ์บ์‹œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + AI-->>Event: ์ฆ‰์‹œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ + else ์บ์‹œ ๋ฏธ์Šค + AI->>AI: Job ID ์ƒ์„ฑ + AI-->>Event: Job ID ๋ฐ˜ํ™˜ + Event-->>Client: Job ID + ํด๋ง URL + + par ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ + AI->>Claude: ํŠธ๋ Œ๋“œ ๋ถ„์„ ์š”์ฒญ (Circuit Breaker) + Claude-->>AI: ๋ถ„์„ ๊ฒฐ๊ณผ + AI->>Redis: ์ง„ํ–‰๋ฅ  ์—…๋ฐ์ดํŠธ (50%) + + AI->>Claude: ์ด๋ฒคํŠธ ์ถ”์ฒœ ์š”์ฒญ (Circuit Breaker) + Claude-->>AI: ์ถ”์ฒœ ๊ฒฐ๊ณผ + AI->>Redis: ์ง„ํ–‰๋ฅ  ์—…๋ฐ์ดํŠธ (100%) + + AI->>Redis: ๊ฒฐ๊ณผ ์บ์‹ฑ (1์‹œ๊ฐ„) + end + + loop ํด๋ง (5์ดˆ ๊ฐ„๊ฒฉ) + Client->>API_GW: GET /jobs/{jobId} + API_GW->>AI: ์ง„ํ–‰ ์ƒํ™ฉ ์กฐํšŒ + AI->>Redis: ์ง„ํ–‰๋ฅ  ์กฐํšŒ + Redis-->>AI: ์ง„ํ–‰๋ฅ  ๋ฐ˜ํ™˜ + AI-->>Client: ์ง„ํ–‰๋ฅ  ๋˜๋Š” ์ตœ์ข… ๊ฒฐ๊ณผ + end + end +``` + +--- + +#### 3.2.3 Content ์„œ๋น„์Šค (๐Ÿ”ด Critical) + +**์ ์šฉ ํŒจํ„ด**: +1. **Asynchronous Request-Reply** (์ด๋ฏธ์ง€ ์ƒ์„ฑ) + - ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ: 5์ดˆ ์ด์ƒ ์†Œ์š” + - Job ID ๋ฐ˜ํ™˜ โ†’ ํด๋ผ์ด์–ธํŠธ ํด๋ง + +2. **Cache-Aside** (์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€) + - ์˜๊ตฌ ์บ์‹ฑ (S3 + CloudFront CDN) + - ๋™์ผ ์š”์ฒญ ์‹œ CDN URL ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + +3. **Circuit Breaker** (Stable Diffusion API) + - ์žฅ์•  ์‹œ ํด๋ฐฑ: ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์ด๋ฏธ์ง€ ์ œ๊ณต + - Threshold: 5ํšŒ ์—ฐ์† ์‹คํŒจ + - Timeout: 10์ดˆ + +4. **Retry** (Stable Diffusion API) + - ์žฌ์‹œ๋„: 3ํšŒ + - Backoff: 2์ดˆ โ†’ 4์ดˆ โ†’ 8์ดˆ + +**ํ•ต์‹ฌ ํ”Œ๋กœ์šฐ**: +``` +์ด๋ฏธ์ง€ ์ƒ์„ฑ ์š”์ฒญ + โ†’ Job ID ๋ฐ˜ํ™˜ + โ†’ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ + โ†’ Stable Diffusion API ํ˜ธ์ถœ (Circuit Breaker + Retry) + โ†’ ์„ฑ๊ณต: S3 ์—…๋กœ๋“œ + CDN URL ๋ฐ˜ํ™˜ + Redis ์บ์‹ฑ + โ†’ ์‹คํŒจ: ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์ด๋ฏธ์ง€ ๋ฐ˜ํ™˜ + โ†’ ํด๋ผ์ด์–ธํŠธ ํด๋ง์œผ๋กœ ๊ฒฐ๊ณผ ํ™•์ธ +``` + +--- + +#### 3.2.4 Distribution ์„œ๋น„์Šค + +**์ ์šฉ ํŒจํ„ด**: +1. **Bulkhead** (์ฑ„๋„๋ณ„ ๊ฒฉ๋ฆฌ) + - ์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS ๋…๋ฆฝ ์ฒ˜๋ฆฌ + - ํ•˜๋‚˜์˜ ์ฑ„๋„ ์‹คํŒจ๊ฐ€ ๋‹ค๋ฅธ ์ฑ„๋„์— ์˜ํ–ฅ ์—†์Œ + +2. **Circuit Breaker** (์ฑ„๋„๋ณ„ API) + - ์žฅ์•  ์‹œ ํด๋ฐฑ: ํ•ด๋‹น ์ฑ„๋„ ๋ฐฐํฌ ์Šคํ‚ต + - Threshold: 5ํšŒ ์—ฐ์† ์‹คํŒจ + - Timeout: 30์ดˆ + +3. **Retry** (์ฑ„๋„๋ณ„ API) + - ์žฌ์‹œ๋„: 3ํšŒ + - Backoff: 1์ดˆ โ†’ 2์ดˆ โ†’ 4์ดˆ + +**ํ•ต์‹ฌ ํ”Œ๋กœ์šฐ**: +```mermaid +graph LR + Distribution[Distribution ์„œ๋น„์Šค] + + subgraph "๋ณ‘๋ ฌ ๋ฐฐํฌ (Bulkhead)" + UDTV[์šฐ๋ฆฌ๋™๋„คTV
Circuit Breaker] + Ringo[๋ง๊ณ ๋น„์ฆˆ
Circuit Breaker] + Genie[์ง€๋‹ˆTV
Circuit Breaker] + SNS[SNS
Circuit Breaker] + end + + Distribution -->|๋…๋ฆฝ ์ฒ˜๋ฆฌ| UDTV + Distribution -->|๋…๋ฆฝ ์ฒ˜๋ฆฌ| Ringo + Distribution -->|๋…๋ฆฝ ์ฒ˜๋ฆฌ| Genie + Distribution -->|๋…๋ฆฝ ์ฒ˜๋ฆฌ| SNS + + UDTV -->|์„ฑ๊ณต/์‹คํŒจ| Result[๋ฐฐํฌ ๊ฒฐ๊ณผ ์ง‘๊ณ„] + Ringo -->|์„ฑ๊ณต/์‹คํŒจ| Result + Genie -->|์„ฑ๊ณต/์‹คํŒจ| Result + SNS -->|์„ฑ๊ณต/์‹คํŒจ| Result +``` + +--- + +#### 3.2.5 Analytics ์„œ๋น„์Šค + +**์ ์šฉ ํŒจํ„ด**: +1. **Cache-Aside** (๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ) + - 5๋ถ„ ๊ฐ„๊ฒฉ ๋ฐ์ดํ„ฐ ์บ์‹ฑ + - ์‹ค์‹œ๊ฐ„์„ฑ vs ์„ฑ๋Šฅ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ + +2. **Materialized View** (Phase 2) + - ๋ณต์žกํ•œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ + - 5๋ถ„ ๊ฐ„๊ฒฉ ์—…๋ฐ์ดํŠธ + +**ํ•ต์‹ฌ ํ”Œ๋กœ์šฐ**: +``` +๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ ์š”์ฒญ + โ†’ Redis ์บ์‹œ ์กฐํšŒ + โ†’ ์บ์‹œ ํžˆํŠธ (5๋ถ„ ์ด๋‚ด): ์ฆ‰์‹œ ๋ฐ˜ํ™˜ + โ†’ ์บ์‹œ ๋ฏธ์Šค: + โ†’ Participation DB ์กฐํšŒ + โ†’ Distribution ์„œ๋น„์Šค API ํ˜ธ์ถœ + โ†’ ์™ธ๋ถ€ API ํ˜ธ์ถœ (์šฐ๋ฆฌ๋™๋„คTV, SNS ํ†ต๊ณ„) + โ†’ ์ง‘๊ณ„ ๊ณ„์‚ฐ + โ†’ Redis ์บ์‹ฑ (5๋ถ„) + โ†’ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ +``` + +--- + +### 3.3 Phase 2 ํ™•์žฅ ๋‹จ๊ณ„ ์•„ํ‚คํ…์ฒ˜ (Queue ์ถ”๊ฐ€) + +```mermaid +graph TB + subgraph "ํด๋ผ์ด์–ธํŠธ" + Mobile[๋ชจ๋ฐ”์ผ ์•ฑ] + Web[์›น ๋ธŒ๋ผ์šฐ์ €] + end + + subgraph "API Gateway ๋ ˆ์ด์–ด" + Gateway[API Gateway] + end + + Mobile --> Gateway + Web --> Gateway + + subgraph "๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค" + Event[Event ์„œ๋น„์Šค] + AI[AI ์„œ๋น„์Šค
Worker Pool] + Content[Content ์„œ๋น„์Šค
Worker Pool] + Distribution[Distribution ์„œ๋น„์Šค
Worker Pool] + end + + Gateway --> Event + + subgraph "๋ฉ”์‹œ์ง€ ํ (Queue-Based Load Leveling)" + AIQueue[AI ์š”์ฒญ ํ] + ContentQueue[์ฝ˜ํ…์ธ  ์š”์ฒญ ํ] + DistQueue[๋ฐฐํฌ ์š”์ฒญ ํ] + end + + Event --> AIQueue + Event --> ContentQueue + Event --> DistQueue + + AIQueue --> AI + ContentQueue --> Content + DistQueue --> Distribution + + subgraph "์บ์‹œ ๋ ˆ์ด์–ด" + Redis[Redis Cache] + end + + AI -.->|์บ์‹œ| Redis + Content -.->|์บ์‹œ| Redis + + classDef queueLayer fill:#fff3e6,stroke:#ff8800,stroke-width:3px + class AIQueue,ContentQueue,DistQueue queueLayer +``` + +**Phase 2 ์ถ”๊ฐ€ ํŒจํ„ด**: +- **Queue-Based Load Leveling**: ํ”ผํฌ ์‹œ๊ฐ„๋Œ€ ๋ถ€ํ•˜ ๋ถ„์‚ฐ +- **Saga**: ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ + +--- + +### 3.4 Phase 3 ๊ณ ๋„ํ™” ๋‹จ๊ณ„ ์•„ํ‚คํ…์ฒ˜ (CQRS ์ถ”๊ฐ€) + +```mermaid +graph TB + subgraph "Analytics ์„œ๋น„์Šค (CQRS)" + AnalyticsAPI[Analytics API] + + subgraph "Command Side (์“ฐ๊ธฐ)" + CommandHandler[Command Handler] + WriteDB[(Write DB
PostgreSQL)] + end + + subgraph "Query Side (์ฝ๊ธฐ)" + QueryHandler[Query Handler] + ReadDB[(Read DB
MongoDB)] + end + end + + AnalyticsAPI --> CommandHandler + AnalyticsAPI --> QueryHandler + + CommandHandler --> WriteDB + WriteDB -.->|์ด๋ฒคํŠธ ๋ฐœํ–‰| EventBus[์ด๋ฒคํŠธ ๋ฒ„์Šค] + EventBus --> ReadDB + QueryHandler --> ReadDB + + Client[ํด๋ผ์ด์–ธํŠธ] --> AnalyticsAPI +``` + +**Phase 3 ์ถ”๊ฐ€ ํŒจํ„ด**: +- **CQRS**: Analytics ์„œ๋น„์Šค ์ฝ๊ธฐ/์“ฐ๊ธฐ ๋ถ„๋ฆฌ +- **Event Sourcing**: Event ์„œ๋น„์Šค ๋ณ€๊ฒฝ ์ด๋ ฅ ์ถ”์  + +--- + +## 4. Phase๋ณ„ ๊ตฌํ˜„ ๋กœ๋“œ๋งต + +### 4.1 Phase 1: MVP (12์ฃผ) + +**๋ชฉํ‘œ**: ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ฐ ์‚ฌ์šฉ์ž ๊ฒ€์ฆ + +| ์ฃผ์ฐจ | ์ž‘์—… ๋‚ด์šฉ | ์ ์šฉ ํŒจํ„ด | ๋‹ด๋‹น ์„œ๋น„์Šค | +|------|----------|----------|------------| +| 1-2 | ์ธํ”„๋ผ ๊ตฌ์ถ• | API Gateway | ์ „์ฒด | +| 3-4 | User ์„œ๋น„์Šค ๊ฐœ๋ฐœ | Cache-Aside, Circuit Breaker, Retry | User | +| 5-6 | Event ์„œ๋น„์Šค ๊ฐœ๋ฐœ | - | Event | +| 7-8 | AI ์„œ๋น„์Šค ๊ฐœ๋ฐœ | Asynchronous Request-Reply, Cache-Aside, Circuit Breaker, Retry | AI | +| 9-10 | Content ์„œ๋น„์Šค ๊ฐœ๋ฐœ | Asynchronous Request-Reply, Cache-Aside, Circuit Breaker, Retry | Content | +| 11 | Distribution ์„œ๋น„์Šค ๊ฐœ๋ฐœ | Circuit Breaker, Retry, Bulkhead | Distribution | +| 12 | Participation & Analytics | Cache-Aside | Participation, Analytics | + +**Phase 1 ์™„๋ฃŒ ๊ธฐ์ค€**: +- โœ… 20๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ ์™„๋ฃŒ +- โœ… AI ์‘๋‹ต ์‹œ๊ฐ„ 10์ดˆ ์ด๋‚ด +- โœ… ์™ธ๋ถ€ API ์žฅ์•  ๊ฒฉ๋ฆฌ +- โœ… ์‚ฌ์šฉ์ž 100๋ช… ์ง€์› + +--- + +### 4.2 Phase 2: ํ™•์žฅ (8์ฃผ) + +**๋ชฉํ‘œ**: ์‚ฌ์šฉ์ž ์ฆ๊ฐ€ ๋Œ€์‘ ๋ฐ ์„ฑ๋Šฅ ์ตœ์ ํ™” + +| ์ฃผ์ฐจ | ์ž‘์—… ๋‚ด์šฉ | ์ ์šฉ ํŒจํ„ด | ๋‹ด๋‹น ์„œ๋น„์Šค | +|------|----------|----------|------------| +| 1-2 | ๋ฉ”์‹œ์ง€ ํ ๋„์ž… | Queue-Based Load Leveling | AI, Content, Distribution | +| 3-4 | Saga ํŒจํ„ด ๊ตฌํ˜„ | Saga | Event, AI, Content, Distribution | +| 5-6 | ์ˆ˜ํ‰ ํ™•์žฅ ํ…Œ์ŠคํŠธ | - | ์ „์ฒด | +| 7-8 | ๋ชจ๋‹ˆํ„ฐ๋ง ๊ฐ•ํ™” | - | ์ „์ฒด | + +**Phase 2 ์™„๋ฃŒ ๊ธฐ์ค€**: +- โœ… ๋™์‹œ ์ด๋ฒคํŠธ ์ƒ์„ฑ 50๊ฐœ ์ง€์› +- โœ… ์‚ฌ์šฉ์ž 1,000๋ช… ์ง€์› +- โœ… ํ”ผํฌ ์‹œ๊ฐ„๋Œ€ ์•ˆ์ •์„ฑ ํ™•๋ณด + +--- + +### 4.3 Phase 3: ๊ณ ๋„ํ™” (8์ฃผ) + +**๋ชฉํ‘œ**: ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + +| ์ฃผ์ฐจ | ์ž‘์—… ๋‚ด์šฉ | ์ ์šฉ ํŒจํ„ด | ๋‹ด๋‹น ์„œ๋น„์Šค | +|------|----------|----------|------------| +| 1-3 | CQRS ๊ตฌํ˜„ | CQRS | Analytics | +| 4-6 | Event Sourcing ๊ตฌํ˜„ | Event Sourcing | Event | +| 7-8 | ์„ฑ๋Šฅ ํŠœ๋‹ | - | ์ „์ฒด | + +**Phase 3 ์™„๋ฃŒ ๊ธฐ์ค€**: +- โœ… ๋Œ€์‹œ๋ณด๋“œ ๋กœ๋”ฉ 1์ดˆ ์ด๋‚ด +- โœ… ์‚ฌ์šฉ์ž 10,000๋ช… ์ง€์› +- โœ… ๊ฐ์‚ฌ ์ถ”์  ๊ธฐ๋Šฅ + +--- + +## 5. ์˜ˆ์ƒ ์„ฑ๊ณผ ์ง€ํ‘œ + +### 5.1 ์„ฑ๋Šฅ ๊ฐœ์„  ํšจ๊ณผ + +| ์ง€ํ‘œ | Before (ํŒจํ„ด ๋ฏธ์ ์šฉ) | After (ํŒจํ„ด ์ ์šฉ) | ๊ฐœ์„ ์œจ | +|------|-------------------|-----------------|-------| +| **AI ์‘๋‹ต ์‹œ๊ฐ„** (์บ์‹œ ํžˆํŠธ) | 10์ดˆ | 0.1์ดˆ | **99%** โ†“ | +| **์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ๊ฐ„** (์บ์‹œ ํžˆํŠธ) | 5์ดˆ | 0.1์ดˆ | **98%** โ†“ | +| **์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ** (์บ์‹œ ํžˆํŠธ) | 2์ดˆ | 0.05์ดˆ | **97.5%** โ†“ | +| **API Gateway ์‘๋‹ต ์‹œ๊ฐ„** | - | 10ms | - | +| **์™ธ๋ถ€ API ์žฅ์•  ๋ณต๊ตฌ ์‹œ๊ฐ„** | ์ˆ˜๋™ ์ฒ˜๋ฆฌ (5๋ถ„+) | ์ž๋™ ํด๋ฐฑ (0.1์ดˆ) | **99.9%** โ†“ | +| **๋Œ€์‹œ๋ณด๋“œ ๋กœ๋”ฉ ์‹œ๊ฐ„** (Phase 3) | 5์ดˆ | 1์ดˆ | **80%** โ†“ | + +### 5.2 ๋น„์šฉ ์ ˆ๊ฐ ํšจ๊ณผ + +| ํ•ญ๋ชฉ | Before (์›”๊ฐ„) | After (์›”๊ฐ„) | ์ ˆ๊ฐ์œจ | +|------|-------------|------------|-------| +| **AI API ํ˜ธ์ถœ ๋น„์šฉ** | $1,000 | $100 | **90%** โ†“ | +| **์ด๋ฏธ์ง€ ์ƒ์„ฑ API ๋น„์šฉ** | $500 | $50 | **90%** โ†“ | +| **๊ตญ์„ธ์ฒญ API ํ˜ธ์ถœ ๋น„์šฉ** | $200 | $20 | **90%** โ†“ | +| **์„œ๋ฒ„ ์ธํ”„๋ผ ๋น„์šฉ** (Phase 2) | $500 | $300 | **40%** โ†“ | +| **์ด ์šด์˜ ๋น„์šฉ** | $2,200 | $470 | **78.6%** โ†“ | + +**์—ฐ๊ฐ„ ์ ˆ๊ฐ์•ก**: $20,760 + +### 5.3 ์•ˆ์ •์„ฑ ๊ฐœ์„  ํšจ๊ณผ + +| ์ง€ํ‘œ | Before | After | ๊ฐœ์„ ์œจ | +|------|--------|-------|-------| +| **์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ** | 95% | 99.9% | **4.9%** โ†‘ | +| **์™ธ๋ถ€ API ์žฅ์•  ์˜ํ–ฅ๋„** | ์ „์ฒด ์„œ๋น„์Šค ์ค‘๋‹จ | ํ•ด๋‹น ๊ธฐ๋Šฅ๋งŒ ํด๋ฐฑ | **99%** โ†“ | +| **ํ‰๊ท  ๋ณต๊ตฌ ์‹œ๊ฐ„ (MTTR)** | 30๋ถ„ | 0.1์ดˆ (์ž๋™) | **99.9%** โ†“ | +| **์‚ฌ์šฉ์ž ์ดํƒˆ๋ฅ ** (์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ) | 80% | 5% | **93.75%** โ†“ | + +### 5.4 ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ ํšจ๊ณผ + +| ์ง€ํ‘œ | Before | After | ๊ฐœ์„ ์œจ | +|------|--------|-------|-------| +| **์™ธ๋ถ€ API ์—ฐ๋™ ์‹œ๊ฐ„** | 2์ฃผ | 3์ผ | **78.6%** โ†“ | +| **์—๋Ÿฌ ์ฒ˜๋ฆฌ ์ฝ”๋“œ ์ž‘์„ฑ ์‹œ๊ฐ„** | 1์ฃผ | 1์ผ | **85.7%** โ†“ | +| **๋ชจ๋‹ˆํ„ฐ๋ง ๊ตฌ์ถ• ์‹œ๊ฐ„** | 2์ฃผ | 3์ผ | **78.6%** โ†“ | +| **์ด ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„** | 20์ฃผ | 12์ฃผ (Phase 1) | **40%** โ†“ | + +### 5.5 ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„  ํšจ๊ณผ + +| ์ง€ํ‘œ | Before | After | ๊ฐœ์„ ์œจ | +|------|--------|-------|-------| +| **์ด๋ฒคํŠธ ์ƒ์„ฑ ์™„๋ฃŒ ์‹œ๊ฐ„** | 15๋ถ„ | 5๋ถ„ | **66.7%** โ†“ | +| **์‚ฌ์šฉ์ž ๋งŒ์กฑ๋„** | 70์  | 90์  | **28.6%** โ†‘ | +| **์žฌ๋ฐฉ๋ฌธ์œจ** | 40% | 70% | **75%** โ†‘ | +| **์ด๋ฒคํŠธ ์ƒ์„ฑ ์„ฑ๊ณต๋ฅ ** | 80% | 98% | **22.5%** โ†‘ | + +--- + +## 6. ๊ตฌํ˜„ ์‹œ ๊ณ ๋ ค์‚ฌํ•ญ + +### 6.1 Cache-Aside ๊ตฌํ˜„ + +**Redis ์บ์‹œ ํ‚ค ์ „๋žต**: +``` +ai:trend:{์—…์ข…}:{์ง€์—ญ}:{์‹œ์ฆŒ} โ†’ TTL: 1์‹œ๊ฐ„ +ai:recommend:{๋ชฉ์ }:{์—…์ข…} โ†’ TTL: 24์‹œ๊ฐ„ +content:image:{eventId} โ†’ TTL: ์˜๊ตฌ (CDN) +user:bizno:{์‚ฌ์—…์ž๋ฒˆํ˜ธ} โ†’ TTL: 30์ผ +``` + +**์บ์‹œ ๋ฌดํšจํ™” ์ „๋žต**: +- AI ํŠธ๋ Œ๋“œ: 1์‹œ๊ฐ„ ์ž๋™ ๋งŒ๋ฃŒ +- ์ด๋ฏธ์ง€: ์˜๊ตฌ ๋ณด๊ด€ (S3 + CDN) +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ: 30์ผ ์ž๋™ ๋งŒ๋ฃŒ ๋˜๋Š” ์žฌ๊ฒ€์ฆ ์š”์ฒญ ์‹œ ๋ฌดํšจํ™” + +### 6.2 Circuit Breaker ์„ค์ • + +**Resilience4j ์„ค์ • ์˜ˆ์‹œ**: +```yaml +resilience4j.circuitbreaker: + instances: + claudeAPI: + failureRateThreshold: 50 + waitDurationInOpenState: 60s + slidingWindowSize: 10 + minimumNumberOfCalls: 5 + permittedNumberOfCallsInHalfOpenState: 3 +``` + +**ํด๋ฐฑ ์ „๋žต**: +- **AI API**: ๋ฏธ๋ฆฌ ์ค€๋น„๋œ ํ…œํ”Œ๋ฆฟ ๊ธฐ๋ฐ˜ ์ถ”์ฒœ +- **์ด๋ฏธ์ง€ ์ƒ์„ฑ API**: ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์ด๋ฏธ์ง€ +- **๊ตญ์„ธ์ฒญ API**: ์บ์‹œ๋œ ๊ฒฐ๊ณผ ๋˜๋Š” ์ˆ˜๋™ ์Šน์ธ +- **๋ฐฐํฌ ์ฑ„๋„ API**: ํ•ด๋‹น ์ฑ„๋„ ์Šคํ‚ต, ๋‹ค๋ฅธ ์ฑ„๋„ ๊ณ„์† ์ง„ํ–‰ + +### 6.3 Asynchronous Request-Reply ๊ตฌํ˜„ + +**ํด๋ง vs WebSocket**: +- **Phase 1**: ํด๋ง (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + - ํด๋ผ์ด์–ธํŠธ: 5์ดˆ ๊ฐ„๊ฒฉ ํด๋ง + - ์„œ๋ฒ„: Job ์ƒํƒœ Redis ์ €์žฅ +- **Phase 2**: WebSocket (์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ) + - ์ง„ํ–‰๋ฅ  ์‹ค์‹œ๊ฐ„ ํ‘ธ์‹œ + - ์„œ๋ฒ„ ๋ถ€ํ•˜ ๊ฐ์†Œ + +**Job ์ƒํƒœ ๊ด€๋ฆฌ**: +```json +{ + "jobId": "job-12345", + "status": "processing", + "progress": 50, + "result": null, + "error": null, + "createdAt": "2025-10-21T10:00:00Z", + "updatedAt": "2025-10-21T10:00:05Z" +} +``` + +### 6.4 Bulkhead ๊ตฌํ˜„ + +**์ฑ„๋„๋ณ„ ์Šค๋ ˆ๋“œ ํ’€ ๋ถ„๋ฆฌ**: +```java +@Configuration +public class ThreadPoolConfig { + @Bean("udtv-pool") + public ThreadPoolTaskExecutor udtvThreadPool() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(5); + executor.setQueueCapacity(100); + return executor; + } + + // ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS๋„ ๋™์ผํ•˜๊ฒŒ ๋ถ„๋ฆฌ +} +``` + +--- + +## 7. ์œ„ํ—˜ ์š”์†Œ ๋ฐ ๋Œ€์‘ ๋ฐฉ์•ˆ + +### 7.1 AI API ๋น„์šฉ ํญ๋ฐœ + +**์œ„ํ—˜**: +- ์บ์‹ฑ ๋ฏธ์ ์šฉ ์‹œ AI API ๋น„์šฉ ์›” $10,000+ ๋ฐœ์ƒ ๊ฐ€๋Šฅ + +**๋Œ€์‘ ๋ฐฉ์•ˆ**: +1. Cache-Aside ํŒจํ„ด ํ•„์ˆ˜ ์ ์šฉ +2. ์บ์‹œ ํžˆํŠธ์œจ ๋ชจ๋‹ˆํ„ฐ๋ง (๋ชฉํ‘œ: 90% ์ด์ƒ) +3. ์›”๊ฐ„ ๋น„์šฉ ์ž„๊ณ„๊ฐ’ ์„ค์ • ($500) +4. ์ž„๊ณ„๊ฐ’ ์ดˆ๊ณผ ์‹œ ์•Œ๋ฆผ ๋ฐ ์ž๋™ ์ฐจ๋‹จ + +### 7.2 ์™ธ๋ถ€ API ์˜์กด์„ฑ + +**์œ„ํ—˜**: +- 7๊ฐœ ์™ธ๋ถ€ API ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์žฅ์•  ์‹œ ์„œ๋น„์Šค ์ค‘๋‹จ + +**๋Œ€์‘ ๋ฐฉ์•ˆ**: +1. Circuit Breaker ํŒจํ„ด ํ•„์ˆ˜ ์ ์šฉ +2. ํด๋ฐฑ ์ „๋žต ๋ช…ํ™•ํžˆ ์ •์˜ +3. SLA ๊ณ„์•ฝ ์ฒด๊ฒฐ (์šฐ๋ฆฌ๋™๋„คTV, ์ง€๋‹ˆTV ๋“ฑ) +4. ๋Œ€์ฒด API ์ค€๋น„ (์˜ˆ: Claude โ†” GPT-4 ์ „ํ™˜) + +### 7.3 ์บ์‹œ ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ + +**์œ„ํ—˜**: +- ํŠธ๋ Œ๋“œ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ์บ์‹œ ๋ฌดํšจํ™” ๋ˆ„๋ฝ + +**๋Œ€์‘ ๋ฐฉ์•ˆ**: +1. TTL ๊ธฐ๋ฐ˜ ์ž๋™ ๋งŒ๋ฃŒ +2. ์ˆ˜๋™ ๋ฌดํšจํ™” API ์ œ๊ณต +3. ์บ์‹œ ๋ฒ„์ „ ๊ด€๋ฆฌ + +### 7.4 ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๋ณต์žก๋„ + +**์œ„ํ—˜**: +- Job ์ƒํƒœ ๊ด€๋ฆฌ ์‹คํŒจ, ํด๋ง ๋ถ€ํ•˜ + +**๋Œ€์‘ ๋ฐฉ์•ˆ**: +1. Redis๋ฅผ ํ†ตํ•œ ์•ˆ์ •์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ +2. Job TTL ์„ค์ • (24์‹œ๊ฐ„ ์ž๋™ ์‚ญ์ œ) +3. Phase 2์—์„œ WebSocket์œผ๋กœ ์ „ํ™˜ + +--- + +## 8. ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์šด์˜ + +### 8.1 ํ•ต์‹ฌ ๋ฉ”ํŠธ๋ฆญ + +**์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ**: +- AI ์‘๋‹ต ์‹œ๊ฐ„ (p50, p95, p99) +- ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ๊ฐ„ (p50, p95, p99) +- ์บ์‹œ ํžˆํŠธ์œจ (๋ชฉํ‘œ: 90%) +- API Gateway ์‘๋‹ต ์‹œ๊ฐ„ (๋ชฉํ‘œ: 10ms) + +**์•ˆ์ •์„ฑ ๋ฉ”ํŠธ๋ฆญ**: +- Circuit Breaker ์ƒํƒœ (Open/Half-Open/Closed) +- ์™ธ๋ถ€ API ์‹คํŒจ์œจ (๋ชฉํ‘œ: 1% ์ดํ•˜) +- ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ (๋ชฉํ‘œ: 99.9%) + +**๋น„์šฉ ๋ฉ”ํŠธ๋ฆญ**: +- AI API ํ˜ธ์ถœ ํšŸ์ˆ˜ ๋ฐ ๋น„์šฉ +- ์ด๋ฏธ์ง€ ์ƒ์„ฑ API ํ˜ธ์ถœ ํšŸ์ˆ˜ ๋ฐ ๋น„์šฉ +- ์บ์‹œ ์ ˆ๊ฐ ํšจ๊ณผ (์˜ˆ์ƒ ๋น„์šฉ - ์‹ค์ œ ๋น„์šฉ) + +### 8.2 ์•Œ๋ฆผ ๊ทœ์น™ + +**Critical ์•Œ๋ฆผ**: +- Circuit Breaker Open ์ƒํƒœ +- AI ์‘๋‹ต ์‹œ๊ฐ„ > 20์ดˆ +- ์บ์‹œ ํžˆํŠธ์œจ < 80% +- ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ < 99% + +**Warning ์•Œ๋ฆผ**: +- ์™ธ๋ถ€ API ์‹คํŒจ์œจ > 5% +- ์›”๊ฐ„ AI API ๋น„์šฉ > $500 +- ๋™์‹œ ์ ‘์†์ž > 80% ์ž„๊ณ„๊ฐ’ + +--- + +## 9. ๊ฒฐ๋ก  + +### 9.1 ์„ ์ •๋œ ํŒจํ„ด ์š”์•ฝ + +**Phase 1 (MVP)**: +1. โœ… API Gateway - ์ค‘์•™ ์ธ์ฆ ๋ฐ ๋ผ์šฐํŒ… +2. โœ… Cache-Aside - AI/์ด๋ฏธ์ง€ ๋น„์šฉ 90% ์ ˆ๊ฐ +3. โœ… Asynchronous Request-Reply - ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„  +4. โœ… Circuit Breaker - ์™ธ๋ถ€ API ์žฅ์•  ๊ฒฉ๋ฆฌ +5. โœ… Retry - ์ผ์‹œ์  ์˜ค๋ฅ˜ ์ž๋™ ๋ณต๊ตฌ + +**Phase 2 (ํ™•์žฅ)**: +6. โœ… Queue-Based Load Leveling - ๋ถ€ํ•˜ ๋ถ„์‚ฐ +7. โœ… Saga - ๋ณต์žกํ•œ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ + +**Phase 3 (๊ณ ๋„ํ™”)**: +8. โœ… CQRS - ๋Œ€์‹œ๋ณด๋“œ ์„ฑ๋Šฅ ์ตœ์ ํ™” +9. โœ… Event Sourcing - ๊ฐ์‚ฌ ์ถ”์  + +### 9.2 ์˜ˆ์ƒ ํšจ๊ณผ + +**์„ฑ๋Šฅ**: +- AI ์‘๋‹ต ์‹œ๊ฐ„: 99% ๊ฐœ์„  (์บ์‹œ ํžˆํŠธ ์‹œ) +- ์ด๋ฏธ์ง€ ์ƒ์„ฑ: 98% ๊ฐœ์„  (์บ์‹œ ํžˆํŠธ ์‹œ) + +**๋น„์šฉ**: +- ์—ฐ๊ฐ„ $20,760 ์ ˆ๊ฐ (78.6% ๊ฐ์†Œ) + +**์•ˆ์ •์„ฑ**: +- ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ: 95% โ†’ 99.9% +- ์™ธ๋ถ€ API ์žฅ์•  ์˜ํ–ฅ: 99% ๊ฐ์†Œ + +**์‚ฌ์šฉ์ž ๊ฒฝํ—˜**: +- ์ด๋ฒคํŠธ ์ƒ์„ฑ ์‹œ๊ฐ„: 66.7% ๋‹จ์ถ• +- ์‚ฌ์šฉ์ž ๋งŒ์กฑ๋„: 28.6% ํ–ฅ์ƒ + +### 9.3 ๋‹ค์Œ ๋‹จ๊ณ„ + +1. **Phase 1 (์ฃผ 1-2)**: API Gateway ๊ตฌ์ถ• +2. **Phase 1 (์ฃผ 3-4)**: User ์„œ๋น„์Šค + Cache-Aside + Circuit Breaker +3. **Phase 1 (์ฃผ 5-6)**: Event ์„œ๋น„์Šค +4. **Phase 1 (์ฃผ 7-8)**: AI ์„œ๋น„์Šค + Asynchronous Request-Reply +5. **Phase 1 (์ฃผ 9-10)**: Content ์„œ๋น„์Šค + Asynchronous Request-Reply +6. **Phase 1 (์ฃผ 11)**: Distribution ์„œ๋น„์Šค + Bulkhead +7. **Phase 1 (์ฃผ 12)**: Participation & Analytics + ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + +--- + +**๋ฌธ์„œ ์ž‘์„ฑ์ž**: System Architect - ๋ฐ•์˜์ž +**๊ฒ€ํ† ์ž**: Backend Developer - ์ตœ์ˆ˜์—ฐ, DevOps Engineer - ์†ก๊ทผ์ • +**์Šน์ธ์ผ**: 2025-10-21 diff --git a/design/pattern/backup/architecture-pattern-backup2.md b/design/pattern/backup/architecture-pattern-backup2.md new file mode 100644 index 0000000..f1fa3f4 --- /dev/null +++ b/design/pattern/backup/architecture-pattern-backup2.md @@ -0,0 +1,1165 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ํด๋ผ์šฐ๋“œ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด ์ ์šฉ ๋ฐฉ์•ˆ + +## ๋ฌธ์„œ ์ •๋ณด +- **์ž‘์„ฑ์ผ**: 2025-10-22 +- **๋ฒ„์ „**: 3.0 (Azure Kubernetes ๊ธฐ๋ฐ˜) +- **์ ์šฉ ํŒจํ„ด**: 4๊ฐœ ํ•ต์‹ฌ ํŒจํ„ด +- **์ธํ”„๋ผ ํ™˜๊ฒฝ**: Azure Kubernetes Service (AKS) +- **๋ชฉํ‘œ**: ๋น ๋ฅธ MVP ์ถœ์‹œ์™€ ์•ˆ์ •์  ์„œ๋น„์Šค ์ œ๊ณต + +--- + +## 1. ์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„ + +### 1.1 ๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ + +**User ์„œ๋น„์Šค** +- ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ๋ฐ ํ”„๋กœํ•„ ๊ด€๋ฆฌ +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ (๊ตญ์„ธ์ฒญ API) + +**Event ์„œ๋น„์Šค** +- ์ด๋ฒคํŠธ CRUD ๋ฐ ์ƒํƒœ ๊ด€๋ฆฌ +- ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ โ†’ AI ์ถ”์ฒœ โ†’ ์ฝ˜ํ…์ธ  ์ƒ์„ฑ โ†’ ๋ฐฐํฌ โ†’ ๋ถ„์„ ํ”Œ๋กœ์šฐ + +**AI ์„œ๋น„์Šค** +- ํŠธ๋ Œ๋“œ ๋ถ„์„ (์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ) +- 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ ์ž๋™ ์ƒ์„ฑ +- Claude API/GPT-4 API ์—ฐ๋™ + +**Content ์„œ๋น„์Šค** +- 3๊ฐ€์ง€ ์Šคํƒ€์ผ SNS ์ด๋ฏธ์ง€ ์ž๋™ ์ƒ์„ฑ +- Stable Diffusion/DALL-E API ์—ฐ๋™ +- ํ”Œ๋žซํผ๋ณ„ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” (Instagram, Naver, Kakao) + +**Distribution ์„œ๋น„์Šค** +- ๋‹ค์ค‘ ์ฑ„๋„ ๋™์‹œ ๋ฐฐํฌ (์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS) +- 7๊ฐœ ์™ธ๋ถ€ API ์—ฐ๋™ + +**Participation ์„œ๋น„์Šค** +- ์ด๋ฒคํŠธ ์ฐธ์—ฌ ์ ‘์ˆ˜ ๋ฐ ๋‹น์ฒจ์ž ์ถ”์ฒจ + +**Analytics ์„œ๋น„์Šค** +- ์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ ๋Œ€์‹œ๋ณด๋“œ +- ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ + +### 1.2 ๋น„๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ + +**์„ฑ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ** +- AI ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ด๋ฒคํŠธ ์ถ”์ฒœ: **10์ดˆ ์ด๋‚ด** +- SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ: **5์ดˆ ์ด๋‚ด** +- ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ: **5๋ถ„ ๊ฐ„๊ฒฉ** +- ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ: **1๋ถ„ ์ด๋‚ด** + +**๊ฐ€์šฉ์„ฑ ์š”๊ตฌ์‚ฌํ•ญ** +- ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ: **99% ์ด์ƒ** (MVP ๋ชฉํ‘œ) +- ์™ธ๋ถ€ API ์žฅ์•  ์‹œ ์„œ๋น„์Šค ์ง€์† ๊ฐ€๋Šฅ + +**ํ™•์žฅ์„ฑ ์š”๊ตฌ์‚ฌํ•ญ** +- ์ดˆ๊ธฐ ์‚ฌ์šฉ์ž: **100๋ช…** +- ๋™์‹œ ์ด๋ฒคํŠธ: **50๊ฐœ** +- ์บ์‹œ ํžˆํŠธ์œจ: **80% ์ด์ƒ** + +**๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ** +- JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ/์ธ๊ฐ€ +- API Gateway๋ฅผ ํ†ตํ•œ ์ค‘์•™์ง‘์ค‘์‹ ๋ณด์•ˆ + +### 1.3 ์™ธ๋ถ€ API ์˜์กด์„ฑ + +| ์„œ๋น„์Šค | ์™ธ๋ถ€ API | ์šฉ๋„ | ์‘๋‹ต์‹œ๊ฐ„ | +|--------|----------|------|----------| +| User | ๊ตญ์„ธ์ฒญ ์‚ฌ์—…์ž๋“ฑ๋ก์ •๋ณด ์ง„์œ„ํ™•์ธ API | ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ | 2-3์ดˆ | +| AI | Claude API / GPT-4 API | ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ | 5-10์ดˆ | +| Content | Stable Diffusion / DALL-E API | SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ | 3-5์ดˆ | +| Distribution | ์šฐ๋ฆฌ๋™๋„คTV API | ์˜์ƒ ์—…๋กœ๋“œ ๋ฐ ์†ก์ถœ | 1-2์ดˆ | +| Distribution | ๋ง๊ณ ๋น„์ฆˆ API | ์—ฐ๊ฒฐ์Œ ์—…๋ฐ์ดํŠธ | 1์ดˆ | +| Distribution | ์ง€๋‹ˆTV ๊ด‘๊ณ  API | ๊ด‘๊ณ  ๋“ฑ๋ก | 1-2์ดˆ | +| Distribution | SNS API (Instagram, Naver, Kakao) | ์ž๋™ ํฌ์ŠคํŒ… | 1-3์ดˆ | + +--- + +## 2. ์ ์šฉ ํŒจํ„ด ๊ฐœ์š” + +### 2.1 ์„ ์ •๋œ 4๊ฐ€์ง€ ํ•ต์‹ฌ ํŒจํ„ด + +| ํŒจํ„ด | ์ ์šฉ ๋ชฉ์  | Azure ์„œ๋น„์Šค | ์šฐ์„ ์ˆœ์œ„ | +|------|----------|-------------|---------| +| **Cache-Aside** | AI ์‘๋‹ต ์‹œ๊ฐ„ ๋‹จ์ถ• ๋ฐ ๋น„์šฉ ์ ˆ๊ฐ | Azure Cache for Redis | ๐Ÿ”ด Critical | +| **API Gateway** | ์ค‘์•™์ง‘์ค‘์‹ ๋ณด์•ˆ ๋ฐ ๋ผ์šฐํŒ… | Azure API Management / Nginx Ingress | ๐Ÿ”ด Critical | +| **Asynchronous Request-Reply** | ์žฅ์‹œ๊ฐ„ ์ž‘์—… ์‘๋‹ต ์‹œ๊ฐ„ ๊ฐœ์„  | Azure Service Bus / RabbitMQ | ๐Ÿ”ด Critical | +| **Circuit Breaker** | ์™ธ๋ถ€ API ์žฅ์•  ๊ฒฉ๋ฆฌ | Resilience4j / Polly | ๐ŸŸก Optional | + +### 2.2 Azure Kubernetes ํ™˜๊ฒฝ ๊ณ ๋ ค์‚ฌํ•ญ + +**์ธํ”„๋ผ ๊ตฌ์„ฑ**: +- **AKS (Azure Kubernetes Service)**: ์ปจํ…Œ์ด๋„ˆ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ +- **Azure Cache for Redis**: ๋ถ„์‚ฐ ์บ์‹œ +- **Azure Container Registry (ACR)**: ์ปจํ…Œ์ด๋„ˆ ์ด๋ฏธ์ง€ ์ €์žฅ์†Œ +- **Azure Database for PostgreSQL**: ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค +- **Azure Service Bus**: ๋ฉ”์‹œ์ง€ ํ (Optional, RabbitMQ ๋Œ€์ฒด ๊ฐ€๋Šฅ) +- **Azure Monitor + Application Insights**: ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น… +- **Azure API Management**: API Gateway (Optional, Nginx Ingress ๋Œ€์ฒด ๊ฐ€๋Šฅ) + +**๊ฐœ๋ฐœ ํ™˜๊ฒฝ vs ์šด์˜ ํ™˜๊ฒฝ**: +- **๊ฐœ๋ฐœ ํ™˜๊ฒฝ**: AKS ๊ฐœ๋ฐœ ํด๋Ÿฌ์Šคํ„ฐ, Redis Standard tier, PostgreSQL Basic tier +- **์šด์˜ ํ™˜๊ฒฝ**: AKS ์šด์˜ ํด๋Ÿฌ์Šคํ„ฐ (๋‹ค์ค‘ ๋…ธ๋“œ), Redis Premium tier (HA), PostgreSQL General Purpose (HA) + +--- + +## 3. ํŒจํ„ด๋ณ„ ์ƒ์„ธ ์„ค๊ณ„ + +### 3.1 Cache-Aside ํŒจํ„ด + +#### 3.1.1 ๊ฐœ์š” + +**๋ชฉ์ **: AI API ํ˜ธ์ถœ ๋น„์šฉ ์ ˆ๊ฐ ๋ฐ ์‘๋‹ต ์‹œ๊ฐ„ ๋‹จ์ถ• + +**์ ์šฉ ๋Œ€์ƒ**: +- AI ์„œ๋น„์Šค: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ, ์ด๋ฒคํŠธ ์ถ”์ฒœ ๊ฒฐ๊ณผ +- Content ์„œ๋น„์Šค: ์ƒ์„ฑ๋œ SNS ์ด๋ฏธ์ง€ +- User ์„œ๋น„์Šค: ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ๊ฒฐ๊ณผ + +#### 3.1.2 Azure ๊ตฌํ˜„ ๋ฐฉ์‹ + +**Azure Cache for Redis ๊ตฌ์„ฑ**: +```yaml +# ๊ฐœ๋ฐœ ํ™˜๊ฒฝ +tier: Standard +capacity: C1 (1GB) +availability: Single instance + +# ์šด์˜ ํ™˜๊ฒฝ +tier: Premium +capacity: P1 (6GB) +availability: Zone redundant (๊ณ ๊ฐ€์šฉ์„ฑ) +``` + +**๋™์ž‘ ํ๋ฆ„**: +``` +1. ์š”์ฒญ ์ˆ˜์‹  โ†’ Redis GET {key} +2. Cache HIT: + - ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (์‘๋‹ต ์‹œ๊ฐ„ ~100ms) +3. Cache MISS: + - ์™ธ๋ถ€ API ํ˜ธ์ถœ (์‘๋‹ต ์‹œ๊ฐ„ 5-10์ดˆ) + - Redis SET {key} {value} EX {TTL} + - ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ +``` + +**์บ์‹œ ํ‚ค ์„ค๊ณ„**: +``` +ai:trend:{industry}:{region}:{season} +ai:recommendation:{industry}:{region}:{purpose} +content:image:{event_id}:{style} +user:business:{business_number} +``` + +**TTL ์ •์ฑ…**: +- AI ํŠธ๋ Œ๋“œ ๋ถ„์„: **24์‹œ๊ฐ„** (ํŠธ๋ Œ๋“œ ๋ณ€ํ™” ์ฃผ๊ธฐ ๊ณ ๋ ค) +- AI ์ด๋ฒคํŠธ ์ถ”์ฒœ: **12์‹œ๊ฐ„** (๋นˆ๋ฒˆํ•œ ์—…๋ฐ์ดํŠธ) +- ์ƒ์„ฑ ์ด๋ฏธ์ง€: **7์ผ** (์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ์„ฑ) +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ: **30์ผ** (๋ณ€๊ฒฝ ๋นˆ๋„ ๋‚ฎ์Œ) + +#### 3.1.3 Kubernetes ๋ฐฐํฌ + +```yaml +# redis-deployment.yaml (๊ฐœ๋ฐœ/์šด์˜ ๊ณตํ†ต) +apiVersion: v1 +kind: Service +metadata: + name: redis-cache +spec: + type: LoadBalancer + ports: + - port: 6379 + selector: + app: redis + +--- +# Azure Cache for Redis ์—ฐ๊ฒฐ ์‹œ Secret +apiVersion: v1 +kind: Secret +metadata: + name: redis-secret +type: Opaque +data: + connection-string: +``` + +#### 3.1.4 ๊ธฐ๋Œ€ ํšจ๊ณผ + +| ์ง€ํ‘œ | Before | After (์บ์‹œ ํžˆํŠธ) | ๊ฐœ์„ ์œจ | +|------|--------|-----------------|--------| +| AI ์‘๋‹ต ์‹œ๊ฐ„ | 10์ดˆ | 0.1์ดˆ | **99%** | +| ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ๊ฐ„ | 5์ดˆ | 0.1์ดˆ | **98%** | +| AI API ๋น„์šฉ | $600/์›” | $120/์›” | **80% ์ ˆ๊ฐ** | +| ์ด๋ฏธ์ง€ API ๋น„์šฉ | $600/์›” | $120/์›” | **80% ์ ˆ๊ฐ** | + +--- + +### 3.2 API Gateway ํŒจํ„ด + +#### 3.2.1 ๊ฐœ์š” + +**๋ชฉ์ **: ์ค‘์•™์ง‘์ค‘์‹ ์ธ์ฆ, ๋ผ์šฐํŒ…, Rate Limiting + +**์ฃผ์š” ๊ธฐ๋Šฅ**: +1. **์ธ์ฆ/์ธ๊ฐ€**: JWT ํ† ํฐ ๊ฒ€์ฆ +2. **๋ผ์šฐํŒ…**: URL ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค ๋ผ์šฐํŒ… +3. **Rate Limiting**: API ํ˜ธ์ถœ ์ œํ•œ +4. **๋กœ๊น…**: ์ค‘์•™์ง‘์ค‘์‹ ์ ‘๊ทผ ๋กœ๊ทธ +5. **SSL ์ข…๋ฃŒ**: HTTPS ์ฒ˜๋ฆฌ + +#### 3.2.2 Azure ๊ตฌํ˜„ ์˜ต์…˜ + +**์˜ต์…˜ 1: Azure API Management (๊ถŒ์žฅ - ์šด์˜ ํ™˜๊ฒฝ)** +```yaml +ํŠน์ง•: +- ์™„์ „ ๊ด€๋ฆฌํ˜• ์„œ๋น„์Šค +- ๋‚ด์žฅ ์ธ์ฆ/์ธ๊ฐ€, Rate Limiting +- Azure Monitor ํ†ตํ•ฉ +- ๋†’์€ ๋น„์šฉ (์•ฝ $50/์›” ์ด์ƒ) + +์žฅ์ : +- ์šด์˜ ๋ถ€๋‹ด ์ตœ์†Œํ™” +- ๊ธฐ์—…๊ธ‰ ๊ธฐ๋Šฅ (๊ฐœ๋ฐœ์ž ํฌํ„ธ, API ๋ฒ„์ „ ๊ด€๋ฆฌ) +- Azure ์ƒํƒœ๊ณ„ ํ†ตํ•ฉ + +๋‹จ์ : +- ๋น„์šฉ ๋ถ€๋‹ด +- ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ์ œํ•œ +``` + +**์˜ต์…˜ 2: Nginx Ingress Controller (๊ถŒ์žฅ - ๊ฐœ๋ฐœ ํ™˜๊ฒฝ / ๋น„์šฉ ์ ˆ๊ฐ)** +```yaml +ํŠน์ง•: +- AKS ๋‚ด์žฅ Ingress Controller +- ๋ฌด๋ฃŒ (AKS ๋น„์šฉ์— ํฌํ•จ) +- ๋†’์€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• + +์žฅ์ : +- ๋น„์šฉ ํšจ์œจ์  +- Kubernetes ๋„ค์ดํ‹ฐ๋ธŒ +- ์œ ์—ฐํ•œ ์„ค์ • + +๋‹จ์ : +- ์ˆ˜๋™ ์„ค์ • ํ•„์š” +- ์šด์˜ ๋ถ€๋‹ด +``` + +#### 3.2.3 Nginx Ingress ๊ตฌํ˜„ (๊ถŒ์žฅ) + +```yaml +# nginx-ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: api-gateway-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/rate-limit: "100" # 100 req/min + nginx.ingress.kubernetes.io/ssl-redirect: "true" +spec: + ingressClassName: nginx + tls: + - hosts: + - api.kt-event.com + secretName: tls-secret + rules: + - host: api.kt-event.com + http: + paths: + - path: /api/users + pathType: Prefix + backend: + service: + name: user-service + port: + number: 8080 + - path: /api/events + pathType: Prefix + backend: + service: + name: event-service + port: + number: 8080 + - path: /api/ai + pathType: Prefix + backend: + service: + name: ai-service + port: + number: 8080 + - path: /api/content + pathType: Prefix + backend: + service: + name: content-service + port: + number: 8080 + - path: /api/distribution + pathType: Prefix + backend: + service: + name: distribution-service + port: + number: 8080 + - path: /api/participation + pathType: Prefix + backend: + service: + name: participation-service + port: + number: 8080 + - path: /api/analytics + pathType: Prefix + backend: + service: + name: analytics-service + port: + number: 8080 +``` + +**JWT ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด** (๊ฐ ์„œ๋น„์Šค์—์„œ ๊ตฌํ˜„): +```java +// Spring Boot ์˜ˆ์‹œ +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) { + String token = extractToken(request); + if (token != null && validateToken(token)) { + // ์ธ์ฆ ์„ฑ๊ณต + filterChain.doFilter(request, response); + } else { + // 401 Unauthorized + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } +} +``` + +#### 3.2.4 ๊ธฐ๋Œ€ ํšจ๊ณผ + +| ํ•ญ๋ชฉ | ํšจ๊ณผ | +|------|------| +| ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ | ๊ฐ ์„œ๋น„์Šค ์ธ์ฆ ๋กœ์ง ์ œ๊ฑฐ โ†’ ๊ฐœ๋ฐœ ์‹œ๊ฐ„ **30% ๋‹จ์ถ•** | +| ๋ณด์•ˆ ๊ฐ•ํ™” | ์ค‘์•™์ง‘์ค‘์‹ ๋ณด์•ˆ ์ •์ฑ… ๊ด€๋ฆฌ | +| ์šด์˜ ํšจ์œจ | ํ†ตํ•ฉ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น… | +| ๋น„์šฉ ์ ˆ๊ฐ | Nginx Ingress ์‚ฌ์šฉ ์‹œ **๋ฌด๋ฃŒ** (vs Azure API Management $50/์›”) | + +--- + +### 3.3 Asynchronous Request-Reply ํŒจํ„ด + +#### 3.3.1 ๊ฐœ์š” + +**๋ชฉ์ **: ์žฅ์‹œ๊ฐ„ ์ž‘์—…์˜ ์‚ฌ์šฉ์ž ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์ œ๊ฑฐ + +**์ ์šฉ ๋Œ€์ƒ**: +- AI ์„œ๋น„์Šค: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ (10์ดˆ ์†Œ์š”) +- Content ์„œ๋น„์Šค: SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ (5์ดˆ ์†Œ์š”) + +#### 3.3.2 Azure ๊ตฌํ˜„ ๋ฐฉ์‹ + +**์˜ต์…˜ 1: Azure Service Bus (๊ถŒ์žฅ - ์šด์˜ ํ™˜๊ฒฝ)** +```yaml +ํŠน์ง•: +- ์™„์ „ ๊ด€๋ฆฌํ˜• ๋ฉ”์‹œ์ง€ ํ +- ๋†’์€ ์•ˆ์ •์„ฑ ๋ฐ ๊ฐ€์šฉ์„ฑ +- Azure Monitor ํ†ตํ•ฉ + +๋น„์šฉ: +- Basic tier: $0.05/million operations +- Standard tier: $10/์›” ๊ธฐ๋ณธ + $0.80/million operations + +์žฅ์ : +- ์šด์˜ ๋ถ€๋‹ด ์ตœ์†Œํ™” +- ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ์•ˆ์ •์„ฑ +- Azure ์ƒํƒœ๊ณ„ ํ†ตํ•ฉ +``` + +**์˜ต์…˜ 2: RabbitMQ (๊ถŒ์žฅ - ๊ฐœ๋ฐœ ํ™˜๊ฒฝ / ๋น„์šฉ ์ ˆ๊ฐ)** +```yaml +ํŠน์ง•: +- ์˜คํ”ˆ์†Œ์Šค ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค +- AKS์— ์ง์ ‘ ๋ฐฐํฌ +- ๋ฌด๋ฃŒ (์ธํ”„๋ผ ๋น„์šฉ๋งŒ) + +์žฅ์ : +- ๋น„์šฉ ํšจ์œจ์  +- ๋†’์€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• +- Kubernetes ๋„ค์ดํ‹ฐ๋ธŒ ๋ฐฐํฌ + +๋‹จ์ : +- ์šด์˜ ๋ถ€๋‹ด (HA, ๋ฐฑ์—…) +``` + +#### 3.3.3 RabbitMQ ๊ตฌํ˜„ (๊ถŒ์žฅ) + +**Kubernetes ๋ฐฐํฌ**: +```yaml +# rabbitmq-deployment.yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: rabbitmq +spec: + serviceName: rabbitmq + replicas: 1 # ๊ฐœ๋ฐœ: 1, ์šด์˜: 3 (HA) + selector: + matchLabels: + app: rabbitmq + template: + metadata: + labels: + app: rabbitmq + spec: + containers: + - name: rabbitmq + image: rabbitmq:3.12-management + ports: + - containerPort: 5672 + name: amqp + - containerPort: 15672 + name: management + env: + - name: RABBITMQ_DEFAULT_USER + value: "admin" + - name: RABBITMQ_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: rabbitmq-secret + key: password + volumeMounts: + - name: rabbitmq-data + mountPath: /var/lib/rabbitmq + volumeClaimTemplates: + - metadata: + name: rabbitmq-data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 10Gi + +--- +apiVersion: v1 +kind: Service +metadata: + name: rabbitmq +spec: + ports: + - port: 5672 + name: amqp + - port: 15672 + name: management + selector: + app: rabbitmq +``` + +**๋™์ž‘ ํ๋ฆ„**: +``` +1. ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ + โ†’ ์ฆ‰์‹œ Job ID ๋ฐ˜ํ™˜ (์‘๋‹ต ์‹œ๊ฐ„ ~100ms) + +2. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ + โ†’ RabbitMQ์— Job ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ + โ†’ Worker๊ฐ€ Job ์ฒ˜๋ฆฌ (10์ดˆ) + โ†’ ๊ฒฐ๊ณผ๋ฅผ Redis์— ์ €์žฅ + +3. ํด๋ผ์ด์–ธํŠธ ํด๋ง + โ†’ GET /api/jobs/{id} + โ†’ Redis์—์„œ Job ์ƒํƒœ ํ™•์ธ + โ†’ ์™„๋ฃŒ ์‹œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ +``` + +**Job ์ƒํƒœ ๊ด€๋ฆฌ** (Redis): +``` +job:{job_id}:status = "pending" | "processing" | "completed" | "failed" +job:{job_id}:result = {result_data} +job:{job_id}:progress = 0-100 (%) +TTL: 1์‹œ๊ฐ„ +``` + +#### 3.3.4 Spring Boot ๊ตฌํ˜„ ์˜ˆ์‹œ + +```java +// AI ์„œ๋น„์Šค - Job ์ƒ์„ฑ +@PostMapping("/api/ai/recommendations") +public ResponseEntity createRecommendationJob(@RequestBody RecommendationRequest request) { + String jobId = UUID.randomUUID().toString(); + + // Redis์— Job ์ƒํƒœ ์ €์žฅ + redisTemplate.opsForValue().set("job:" + jobId + ":status", "pending", 1, TimeUnit.HOURS); + + // RabbitMQ์— Job ๋ฐœํ–‰ + rabbitTemplate.convertAndSend("ai.queue", new JobMessage(jobId, request)); + + return ResponseEntity.ok(new JobResponse(jobId, "pending")); +} + +// Worker - Job ์ฒ˜๋ฆฌ +@RabbitListener(queues = "ai.queue") +public void processRecommendationJob(JobMessage message) { + String jobId = message.getJobId(); + + // Job ์ƒํƒœ ์—…๋ฐ์ดํŠธ: processing + redisTemplate.opsForValue().set("job:" + jobId + ":status", "processing"); + + try { + // AI API ํ˜ธ์ถœ (10์ดˆ ์†Œ์š”) + String result = callClaudeAPI(message.getRequest()); + + // ๊ฒฐ๊ณผ ์ €์žฅ + redisTemplate.opsForValue().set("job:" + jobId + ":result", result, 1, TimeUnit.HOURS); + redisTemplate.opsForValue().set("job:" + jobId + ":status", "completed"); + + // ์บ์‹œ์—๋„ ์ €์žฅ (Cache-Aside) + String cacheKey = "ai:recommendation:" + getCacheKey(message.getRequest()); + redisTemplate.opsForValue().set(cacheKey, result, 24, TimeUnit.HOURS); + + } catch (Exception e) { + redisTemplate.opsForValue().set("job:" + jobId + ":status", "failed"); + redisTemplate.opsForValue().set("job:" + jobId + ":error", e.getMessage()); + } +} + +// Job ์ƒํƒœ ์กฐํšŒ +@GetMapping("/api/jobs/{jobId}") +public ResponseEntity getJobStatus(@PathVariable String jobId) { + String status = redisTemplate.opsForValue().get("job:" + jobId + ":status"); + + if ("completed".equals(status)) { + String result = redisTemplate.opsForValue().get("job:" + jobId + ":result"); + return ResponseEntity.ok(new JobStatusResponse(jobId, status, result)); + } else { + return ResponseEntity.ok(new JobStatusResponse(jobId, status, null)); + } +} +``` + +#### 3.3.5 ๊ธฐ๋Œ€ ํšจ๊ณผ + +| ์ง€ํ‘œ | Before | After | ๊ฐœ์„  | +|------|--------|-------|------| +| ์‚ฌ์šฉ์ž ๋Œ€๊ธฐ ์‹œ๊ฐ„ | 10์ดˆ | 0.1์ดˆ (์ฆ‰์‹œ ์‘๋‹ต) | **99% ๊ฐœ์„ ** | +| ๋™์‹œ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ ์š”์ฒญ | 10 req/sec | 50 req/sec | **5๋ฐฐ ์ฆ๊ฐ€** | +| ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค ํšจ์œจ | ๋™๊ธฐ ๋Œ€๊ธฐ๋กœ ๋‚ญ๋น„ | ์ตœ์ ํ™” | **30% ์ ˆ๊ฐ** | + +--- + +### 3.4 Circuit Breaker ํŒจํ„ด (Optional) + +#### 3.4.1 ๊ฐœ์š” + +**๋ชฉ์ **: ์™ธ๋ถ€ API ์žฅ์•  ์‹œ ์„œ๋น„์Šค ๊ฒฉ๋ฆฌ ๋ฐ ๋น ๋ฅธ ์‹คํŒจ + +**์ ์šฉ ๋Œ€์ƒ**: +- ๋ชจ๋“  ์™ธ๋ถ€ API ์—ฐ๋™ ์ง€์  (7๊ฐœ API) + +#### 3.4.2 ๊ตฌํ˜„ ๋ฐฉ์‹ (Resilience4j) + +**์˜์กด์„ฑ ์ถ”๊ฐ€** (Spring Boot): +```xml + + io.github.resilience4j + resilience4j-spring-boot3 + 2.1.0 + +``` + +**์„ค์ •** (application.yml): +```yaml +resilience4j: + circuitbreaker: + configs: + default: + slidingWindowSize: 10 + minimumNumberOfCalls: 5 + failureRateThreshold: 50 # 50% ์‹คํŒจ ์‹œ Open + waitDurationInOpenState: 30s + permittedNumberOfCallsInHalfOpenState: 3 + instances: + claudeAPI: + baseConfig: default + stableDiffusionAPI: + baseConfig: default + taxAPI: + baseConfig: default + distributionAPIs: + baseConfig: default +``` + +**์ฝ”๋“œ ์˜ˆ์‹œ**: +```java +@Service +public class ClaudeAPIClient { + + @CircuitBreaker(name = "claudeAPI", fallbackMethod = "getRecommendationFallback") + public String getRecommendation(RecommendationRequest request) { + // Claude API ํ˜ธ์ถœ + return restTemplate.postForObject(CLAUDE_API_URL, request, String.class); + } + + // Fallback: ์บ์‹œ๋œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ๋˜๋Š” ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€ + public String getRecommendationFallback(RecommendationRequest request, Exception e) { + log.warn("Claude API circuit breaker activated, using fallback", e); + + // ์บ์‹œ์—์„œ ์œ ์‚ฌํ•œ ์ด์ „ ๊ฒฐ๊ณผ ์กฐํšŒ + String cachedResult = getCachedRecommendation(request); + + if (cachedResult != null) { + return cachedResult; + } + + // ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€ + return "AI ์„œ๋น„์Šค๊ฐ€ ์ผ์‹œ์ ์œผ๋กœ ์ด์šฉ ๋ถˆ๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."; + } +} +``` + +#### 3.4.3 Fallback ์ „๋žต + +| ์„œ๋น„์Šค | ์™ธ๋ถ€ API | Fallback ์ „๋žต | +|--------|---------|--------------| +| AI | Claude API | ์บ์‹œ๋œ ์ด์ „ ์ถ”์ฒœ ๊ฒฐ๊ณผ + ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ | +| Content | Stable Diffusion | ๊ธฐ๋ณธ ํ…œํ”Œ๋ฆฟ ์ด๋ฏธ์ง€ + ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ | +| User | ๊ตญ์„ธ์ฒญ API | ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ์Šคํ‚ต (์ˆ˜๋™ ํ™•์ธ์œผ๋กœ ๋Œ€์ฒด) | +| Distribution | ๊ฐ ์ฑ„๋„ API | ํ•ด๋‹น ์ฑ„๋„ ๋ฐฐํฌ ์Šคํ‚ต + ์•Œ๋ฆผ | + +#### 3.4.4 ๋ชจ๋‹ˆํ„ฐ๋ง (Azure Monitor) + +**์ปค์Šคํ…€ ๋ฉ”ํŠธ๋ฆญ**: +```java +@Component +public class CircuitBreakerMetrics { + + private final MeterRegistry meterRegistry; + private final CircuitBreakerRegistry circuitBreakerRegistry; + + @PostConstruct + public void registerMetrics() { + circuitBreakerRegistry.getAllCircuitBreakers().forEach(cb -> { + Gauge.builder("circuit.breaker.state", cb, + this::getStateValue) + .tag("name", cb.getName()) + .register(meterRegistry); + + cb.getEventPublisher() + .onStateTransition(event -> { + log.info("Circuit Breaker {} state changed: {} -> {}", + event.getCircuitBreakerName(), + event.getStateTransition().getFromState(), + event.getStateTransition().getToState()); + }); + }); + } + + private int getStateValue(CircuitBreaker cb) { + switch (cb.getState()) { + case CLOSED: return 0; + case OPEN: return 1; + case HALF_OPEN: return 2; + default: return -1; + } + } +} +``` + +#### 3.4.5 ๊ธฐ๋Œ€ ํšจ๊ณผ + +| ์ง€ํ‘œ | Before | After | ๊ฐœ์„  | +|------|--------|-------|------| +| ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ | 95% | 99% | **+4%p** | +| ์žฅ์•  ๋ณต๊ตฌ ์‹œ๊ฐ„ | 5๋ถ„ (์ˆ˜๋™) | 30์ดˆ (์ž๋™) | **90% ๋‹จ์ถ•** | +| ์™ธ๋ถ€ API ์žฅ์•  ๊ฒฉ๋ฆฌ | โŒ ์ „์ฒด ์ค‘๋‹จ | โœ… ๋…๋ฆฝ ๋™์ž‘ | - | + +--- + +## 4. ์ „์ฒด ์•„ํ‚คํ…์ฒ˜ + +### 4.1 Azure Kubernetes ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Azure Cloud โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Azure Kubernetes Service (AKS) โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Nginx Ingress Controller โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - JWT ์ธ์ฆ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - Rate Limiting โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - SSL ์ข…๋ฃŒ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ–ผโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚User โ”‚ โ”‚Eventโ”‚ โ”‚ AI โ”‚ โ”‚Content โ”‚ โ”‚ Dist โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Svc โ”‚ โ”‚ Svc โ”‚ โ”‚ Svc โ”‚ โ”‚ Svc โ”‚ โ”‚ Svc โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”ฌโ”€โ”€โ”˜ โ””โ”€โ”€โ”ฌโ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚RabbitMQ โ”‚ (Async Request-Reply) โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Worker โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Azure Cache for Redis (Cache-Aside) โ”‚ โ”‚ +โ”‚ โ”‚ - AI ๊ฒฐ๊ณผ ์บ์‹œ โ”‚ โ”‚ +โ”‚ โ”‚ - ์ด๋ฏธ์ง€ ์บ์‹œ โ”‚ โ”‚ +โ”‚ โ”‚ - Job ์ƒํƒœ ๊ด€๋ฆฌ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Azure Database for PostgreSQL โ”‚ โ”‚ +โ”‚ โ”‚ - ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ โ”‚ โ”‚ +โ”‚ โ”‚ - ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Azure Monitor + Application Insights โ”‚ โ”‚ +โ”‚ โ”‚ - ๋กœ๊ทธ ์ˆ˜์ง‘ โ”‚ โ”‚ +โ”‚ โ”‚ - ๋ฉ”ํŠธ๋ฆญ ๋ชจ๋‹ˆํ„ฐ๋ง โ”‚ โ”‚ +โ”‚ โ”‚ - ์•Œ๋žŒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ์™ธ๋ถ€ API (Circuit Breaker ์ ์šฉ) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๊ตญ์„ธ์ฒญ API โ”‚ Claude API โ”‚ Stable Diffusion โ”‚ ์šฐ๋ฆฌ๋™๋„คTV โ”‚... โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 4.2 ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ (AI ์ถ”์ฒœ ํ”Œ๋กœ์šฐ) + +``` +ํด๋ผ์ด์–ธํŠธ โ†’ Nginx Ingress: POST /api/ai/recommendations +Nginx Ingress โ†’ AI Service: JWT ๊ฒ€์ฆ ํ›„ ๋ผ์šฐํŒ… + +AI Service โ†’ Redis: GET ai:recommendation:{key} + +alt Cache HIT + Redis โ†’ AI Service: ์บ์‹œ๋œ ๊ฒฐ๊ณผ + AI Service โ†’ ํด๋ผ์ด์–ธํŠธ: ์ฆ‰์‹œ ๋ฐ˜ํ™˜ (0.1์ดˆ) +else Cache MISS + AI Service โ†’ Redis: SET job:{id}:status = pending + AI Service โ†’ RabbitMQ: ๋ฐœํ–‰ Job Message + AI Service โ†’ ํด๋ผ์ด์–ธํŠธ: Job ID ๋ฐ˜ํ™˜ (0.1์ดˆ) + + RabbitMQ โ†’ Worker: Job ์ˆ˜์‹  + Worker โ†’ Redis: SET job:{id}:status = processing + + Worker โ†’ Claude API: AI ์š”์ฒญ (Circuit Breaker) + + alt API ์„ฑ๊ณต + Claude API โ†’ Worker: ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ (10์ดˆ) + Worker โ†’ Redis: SET ai:recommendation:{key} (TTL 24h) + Worker โ†’ Redis: SET job:{id}:result + Worker โ†’ Redis: SET job:{id}:status = completed + else API ์‹คํŒจ (Circuit Open) + Worker โ†’ Redis: GET ์œ ์‚ฌ ์บ์‹œ ๊ฒฐ๊ณผ + Worker โ†’ Redis: SET job:{id}:result (Fallback) + Worker โ†’ Redis: SET job:{id}:status = completed + end + + loop 5์ดˆ ๊ฐ„๊ฒฉ ํด๋ง + ํด๋ผ์ด์–ธํŠธ โ†’ Nginx Ingress: GET /api/jobs/{id} + Nginx Ingress โ†’ AI Service: ๋ผ์šฐํŒ… + AI Service โ†’ Redis: GET job:{id}:status + + alt completed + Redis โ†’ AI Service: status + result + AI Service โ†’ ํด๋ผ์ด์–ธํŠธ: ์ตœ์ข… ๊ฒฐ๊ณผ + else processing + Redis โ†’ AI Service: status + progress + AI Service โ†’ ํด๋ผ์ด์–ธํŠธ: ์ง„ํ–‰ ์ƒํ™ฉ (70%) + end + end +end +``` + +--- + +## 5. ๊ฐœ๋ฐœ/์šด์˜ ํ™˜๊ฒฝ ๊ตฌ์„ฑ + +### 5.1 ๊ฐœ๋ฐœ ํ™˜๊ฒฝ + +**AKS ํด๋Ÿฌ์Šคํ„ฐ**: +```yaml +name: aks-dev-cluster +node_pool: + vm_size: Standard_B2s (2 vCPU, 4GB RAM) + node_count: 2 + auto_scaling: false + +estimated_cost: ~$100/์›” +``` + +**Azure Cache for Redis**: +```yaml +tier: Standard +capacity: C1 (1GB) +availability: Single instance + +estimated_cost: ~$20/์›” +``` + +**Azure Database for PostgreSQL**: +```yaml +tier: Basic +compute: B_Gen5_1 (1 vCore) +storage: 50GB + +estimated_cost: ~$30/์›” +``` + +**RabbitMQ**: +```yaml +deployment: StatefulSet (1 replica) +storage: 10GB PVC + +estimated_cost: ํฌํ•จ (AKS ๋…ธ๋“œ) +``` + +**์ด ์˜ˆ์ƒ ๋น„์šฉ**: **~$150/์›”** + +### 5.2 ์šด์˜ ํ™˜๊ฒฝ + +**AKS ํด๋Ÿฌ์Šคํ„ฐ**: +```yaml +name: aks-prod-cluster +node_pool: + vm_size: Standard_D2s_v3 (2 vCPU, 8GB RAM) + node_count: 3 (๋‹ค์ค‘ ๊ฐ€์šฉ ์˜์—ญ) + auto_scaling: true (min: 3, max: 10) + +estimated_cost: ~$300/์›” (๊ธฐ๋ณธ), ~$1,000/์›” (์ตœ๋Œ€) +``` + +**Azure Cache for Redis**: +```yaml +tier: Premium +capacity: P1 (6GB) +availability: Zone redundant (๊ณ ๊ฐ€์šฉ์„ฑ) + +estimated_cost: ~$250/์›” +``` + +**Azure Database for PostgreSQL**: +```yaml +tier: General Purpose +compute: GP_Gen5_2 (2 vCore) +storage: 100GB +availability: Zone redundant + +estimated_cost: ~$150/์›” +``` + +**RabbitMQ**: +```yaml +deployment: StatefulSet (3 replicas, HA) +storage: 30GB PVC per replica + +estimated_cost: ํฌํ•จ (AKS ๋…ธ๋“œ) +``` + +**Azure Monitor + Application Insights**: +```yaml +estimated_cost: ~$50/์›” +``` + +**์ด ์˜ˆ์ƒ ๋น„์šฉ**: **~$750/์›”** (๊ธฐ๋ณธ), **~$1,450/์›”** (์ตœ๋Œ€) + +--- + +## 6. ๊ตฌํ˜„ ๋กœ๋“œ๋งต + +### 6.1 MVP ์ผ์ • (12์ฃผ) + +| ์ฃผ์ฐจ | ์ž‘์—… ๋‚ด์šฉ | ํŒจํ„ด ์ ์šฉ | ๋‹ด๋‹น | +|------|----------|-----------|------| +| **1์ฃผ** | Azure ์ธํ”„๋ผ ๊ตฌ์ถ• | - | DevOps | +| | - AKS ํด๋Ÿฌ์Šคํ„ฐ ์ƒ์„ฑ (๊ฐœ๋ฐœ/์šด์˜) | | | +| | - Azure Cache for Redis ์ƒ์„ฑ | | | +| | - Azure Database for PostgreSQL ์ƒ์„ฑ | | | +| | - Azure Container Registry ์ƒ์„ฑ | | | +| **2์ฃผ** | API Gateway ๊ตฌ์ถ• | API Gateway | DevOps + Backend | +| | - Nginx Ingress Controller ๋ฐฐํฌ | | | +| | - SSL ์ธ์ฆ์„œ ์„ค์ • | | | +| | - JWT ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด ๊ตฌํ˜„ | | | +| **3-4์ฃผ** | User/Event ์„œ๋น„์Šค | Cache-Aside | Backend | +| | - ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ๊ตฌํ˜„ | | | +| | - ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ + ์บ์‹ฑ | | | +| | - ์ด๋ฒคํŠธ CRUD ๊ตฌํ˜„ | | | +| **5-6์ฃผ** | AI ์„œ๋น„์Šค | Async + Cache-Aside | Backend | +| | - Claude API ์—ฐ๋™ | | | +| | - RabbitMQ ๋ฐฐํฌ ๋ฐ Worker ๊ตฌํ˜„ | | | +| | - Job ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ | | | +| | - AI ๊ฒฐ๊ณผ ์บ์‹ฑ (24h TTL) | | | +| **7-8์ฃผ** | Content ์„œ๋น„์Šค | Async + Cache-Aside | Backend | +| | - Stable Diffusion API ์—ฐ๋™ | | | +| | - ์ด๋ฏธ์ง€ ์ƒ์„ฑ Job ์ฒ˜๋ฆฌ | | | +| | - ์ด๋ฏธ์ง€ ์บ์‹ฑ ๋ฐ Azure Blob Storage | | | +| **9-10์ฃผ** | Distribution ์„œ๋น„์Šค | Circuit Breaker | Backend | +| | - 7๊ฐœ ์™ธ๋ถ€ API ์—ฐ๋™ | | | +| | - Resilience4j Circuit Breaker ์ ์šฉ | | | +| | - Fallback ์ „๋žต ๊ตฌํ˜„ | | | +| **11์ฃผ** | Participation/Analytics | Cache-Aside | Backend | +| | - ์ฐธ์—ฌ์ž ๊ด€๋ฆฌ ๋ฐ ์ถ”์ฒจ | | | +| | - ๋Œ€์‹œ๋ณด๋“œ ๋ฐ์ดํ„ฐ ์บ์‹ฑ (5๋ถ„ TTL) | | | +| **12์ฃผ** | ํ…Œ์ŠคํŠธ ๋ฐ ์ถœ์‹œ | ์ „์ฒด ํŒจํ„ด ๊ฒ€์ฆ | ์ „์ฒด ํŒ€ | +| | - ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ (k6, 100๋ช…) | | | +| | - ์žฅ์•  ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ | | | +| | - Azure Monitor ์•Œ๋žŒ ์„ค์ • | | | +| | - MVP ์ถœ์‹œ | | | + +### 6.2 ๊ธฐ์ˆ  ์Šคํƒ + +**๋ฐฑ์—”๋“œ**: +- Java 17 + Spring Boot 3.2 +- Resilience4j 2.1 (Circuit Breaker) +- Spring Data JPA + PostgreSQL +- Spring Data Redis +- Spring AMQP + RabbitMQ + +**์ปจํ…Œ์ด๋„ˆ & ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜**: +- Docker 24 +- Kubernetes 1.28 (AKS) +- Nginx Ingress Controller 1.9 +- Helm 3.13 + +**Azure ์„œ๋น„์Šค**: +- Azure Kubernetes Service (AKS) +- Azure Cache for Redis +- Azure Database for PostgreSQL +- Azure Container Registry (ACR) +- Azure Monitor + Application Insights +- Azure Blob Storage (์ด๋ฏธ์ง€ ์ €์žฅ) + +**๋ชจ๋‹ˆํ„ฐ๋ง & ๋กœ๊น…**: +- Azure Monitor (๋ฉ”ํŠธ๋ฆญ) +- Application Insights (APM) +- Grafana (๋Œ€์‹œ๋ณด๋“œ) +- Azure Log Analytics (๋กœ๊ทธ ์ฟผ๋ฆฌ) + +--- + +## 7. ์˜ˆ์ƒ ์„ฑ๊ณผ + +### 7.1 ์„ฑ๋Šฅ ๊ฐœ์„  + +| ํ•ญ๋ชฉ | Before | After (์บ์‹œ ํžˆํŠธ) | ๊ฐœ์„ ์œจ | +|------|--------|-----------------|--------| +| AI ์‘๋‹ต ์‹œ๊ฐ„ | 10์ดˆ | 0.1์ดˆ | **99%** | +| ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์‹œ๊ฐ„ | 5์ดˆ | 0.1์ดˆ | **98%** | +| ๋Œ€์‹œ๋ณด๋“œ ๋กœ๋”ฉ ์‹œ๊ฐ„ | 3์ดˆ | 0.5์ดˆ | **83%** | +| ๋™์‹œ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ ์š”์ฒญ | 10 req/sec | 50 req/sec | **5๋ฐฐ** | + +### 7.2 ๋น„์šฉ ์ ˆ๊ฐ + +**AI API ๋น„์šฉ** (์บ์‹œ ํžˆํŠธ์œจ 80% ๊ฐ€์ •): +- Before: $600/์›” +- After: $120/์›” +- **์ ˆ๊ฐ์•ก**: $480/์›” (**80% ์ ˆ๊ฐ**) + +**์ด๋ฏธ์ง€ API ๋น„์šฉ** (์บ์‹œ ํžˆํŠธ์œจ 80% ๊ฐ€์ •): +- Before: $600/์›” +- After: $120/์›” +- **์ ˆ๊ฐ์•ก**: $480/์›” (**80% ์ ˆ๊ฐ**) + +**์ด API ๋น„์šฉ ์ ˆ๊ฐ**: **$960/์›”** (**80% ์ ˆ๊ฐ**) + +**Azure ์ธํ”„๋ผ ๋น„์šฉ**: +- ๊ฐœ๋ฐœ ํ™˜๊ฒฝ: ~$150/์›” +- ์šด์˜ ํ™˜๊ฒฝ: ~$750/์›” (๊ธฐ๋ณธ), ~$1,450/์›” (์ตœ๋Œ€) + +### 7.3 ๊ฐ€์šฉ์„ฑ ๊ฐœ์„  + +| ์ง€ํ‘œ | Before | After | ๊ฐœ์„  | +|------|--------|-------|------| +| ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ | 95% | 99% | **+4%p** | +| ์™ธ๋ถ€ API ์žฅ์•  ์‹œ ์„œ๋น„์Šค ์ง€์† | โŒ ๋ถˆ๊ฐ€ | โœ… ๊ฐ€๋Šฅ | - | +| ์žฅ์•  ์ž๋™ ๋ณต๊ตฌ ์‹œ๊ฐ„ | 5๋ถ„ (์ˆ˜๋™) | 30์ดˆ (์ž๋™) | **90% ๋‹จ์ถ•** | + +--- + +## 8. ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์šด์˜ + +### 8.1 Azure Monitor ๋ฉ”ํŠธ๋ฆญ + +**AKS ํด๋Ÿฌ์Šคํ„ฐ**: +- CPU/๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋ฅ  +- ๋…ธ๋“œ ์ƒํƒœ +- Pod ์ƒํƒœ +- Ingress ์š”์ฒญ ์ˆ˜ ๋ฐ ์‘๋‹ต ์‹œ๊ฐ„ + +**Azure Cache for Redis**: +- ์บ์‹œ ํžˆํŠธ์œจ (๋ชฉํ‘œ: 80% ์ด์ƒ) +- ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋ฅ  (๋ชฉํ‘œ: 70% ์ดํ•˜) +- ์‘๋‹ต ์‹œ๊ฐ„ (๋ชฉํ‘œ: 100ms ์ดํ•˜) +- ์—ฐ๊ฒฐ ์ˆ˜ + +**Azure Database for PostgreSQL**: +- CPU/๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋ฅ  +- ๋””์Šคํฌ I/O +- ํ™œ์„ฑ ์—ฐ๊ฒฐ ์ˆ˜ +- ์Šฌ๋กœ์šฐ ์ฟผ๋ฆฌ + +**Application Insights (APM)**: +- ์„œ๋น„์Šค๋ณ„ ์‘๋‹ต ์‹œ๊ฐ„ +- ์˜์กด์„ฑ ํ˜ธ์ถœ (์™ธ๋ถ€ API) +- ์˜ˆ์™ธ ๋ฐ ์—๋Ÿฌ +- ์‚ฌ์šฉ์ž ํŠธ๋ž˜ํ”ฝ + +### 8.2 ์ปค์Šคํ…€ ๋ฉ”ํŠธ๋ฆญ + +**Circuit Breaker**: +- API๋ณ„ ์‹คํŒจ์œจ +- Circuit ์ƒํƒœ (Closed/Open/Half-Open) +- Fallback ํ˜ธ์ถœ ํšŸ์ˆ˜ + +**Async Jobs**: +- Job ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ +- ๋™์‹œ Job ์ˆ˜ +- Job ์™„๋ฃŒ์œจ +- Queue ๊ธธ์ด (RabbitMQ) + +### 8.3 ์•Œ๋žŒ ์ž„๊ณ„๊ฐ’ + +| ์ง€ํ‘œ | Warning | Critical | ์•ก์…˜ | +|------|---------|----------|------| +| ์บ์‹œ ํžˆํŠธ์œจ | < 70% | < 50% | ์บ์‹œ ์ „๋žต ๊ฒ€ํ†  | +| Redis ๋ฉ”๋ชจ๋ฆฌ | > 80% | > 90% | ์Šค์ผ€์ผ์—… | +| API ์‘๋‹ต ์‹œ๊ฐ„ | > 500ms | > 1000ms | ์„ฑ๋Šฅ ์กฐ์‚ฌ | +| Circuit Breaker Open | 1๊ฐœ | 3๊ฐœ ์ด์ƒ | ๊ธด๊ธ‰ ๋Œ€์‘ | +| Pod CPU | > 70% | > 90% | Pod ์Šค์ผ€์ผ์•„์›ƒ | +| Pod ๋ฉ”๋ชจ๋ฆฌ | > 80% | > 95% | Pod ์žฌ์‹œ์ž‘ | + +### 8.4 ๋กœ๊ทธ ์ˆ˜์ง‘ ๋ฐ ๋ถ„์„ + +**Azure Log Analytics**: +```kusto +// ์—๋Ÿฌ ๋กœ๊ทธ ๋ถ„์„ +ContainerLog +| where LogEntry contains "ERROR" +| summarize count() by Computer, ContainerID +| order by count_ desc + +// API ์‘๋‹ต ์‹œ๊ฐ„ ๋ถ„์„ +requests +| where url contains "/api/" +| summarize avg(duration), percentile(duration, 95) by name +| order by avg_duration desc + +// Circuit Breaker ์ƒํƒœ ๋ณ€ํ™” ์ถ”์  +traces +| where message contains "Circuit Breaker" +| project timestamp, message, severityLevel +| order by timestamp desc +``` + +--- + +## 9. ์žฅ์•  ๋Œ€์‘ ์ ˆ์ฐจ + +### 9.1 Circuit Breaker Open ๋ฐœ์ƒ + +**๋Œ€์‘ ์ ˆ์ฐจ**: +1. ์•Œ๋žŒ ์ˆ˜์‹  (Azure Monitor โ†’ Slack/Email) +2. Application Insights์—์„œ ํ•ด๋‹น ์™ธ๋ถ€ API ํ˜ธ์ถœ ํ˜„ํ™ฉ ํ™•์ธ +3. Fallback ์ „๋žต ์ •์ƒ ๋™์ž‘ ํ™•์ธ (๋กœ๊ทธ ๋ถ„์„) +4. 30์ดˆ ํ›„ ์ž๋™ Half-Open ์ „ํ™˜ ํ™•์ธ +5. ๋ณต๊ตฌ ์‹คํŒจ ์‹œ: + - ์™ธ๋ถ€ API ์ œ๊ณต์‚ฌ ์ƒํƒœ ํ™•์ธ + - ๊ธด๊ธ‰ ์—ฐ๋ฝ + - ์žฅ๊ธฐ ์žฅ์•  ์‹œ ์‚ฌ์šฉ์ž ๊ณต์ง€ + +### 9.2 ์บ์‹œ ์žฅ์•  + +**๋Œ€์‘ ์ ˆ์ฐจ**: +1. Azure Cache for Redis ์ƒํƒœ ํ™•์ธ (Azure Portal) +2. Zone Redundancy ์ž๋™ Failover ํ™•์ธ (์šด์˜ ํ™˜๊ฒฝ) +3. ์บ์‹œ ๋ฏธ์Šค๋กœ ์ „ํ™˜ (์„ฑ๋Šฅ ์ €ํ•˜ ํ—ˆ์šฉ) +4. Redis ๋ณต๊ตฌ: + - ๊ฐœ๋ฐœ: ์ธ์Šคํ„ด์Šค ์žฌ์‹œ์ž‘ + - ์šด์˜: Azure ์ž๋™ ๋ณต๊ตฌ ๋Œ€๊ธฐ ๋˜๋Š” ์ˆ˜๋™ Failover + +### 9.3 AKS ๋…ธ๋“œ ์žฅ์•  + +**๋Œ€์‘ ์ ˆ์ฐจ**: +1. Azure Monitor์—์„œ ๋…ธ๋“œ ์ƒํƒœ ํ™•์ธ +2. Kubernetes ์ž๋™ ์žฌ์Šค์ผ€์ค„๋ง ํ™•์ธ +3. Pod๊ฐ€ ์ •์ƒ ๋…ธ๋“œ๋กœ ์ด๋™ ํ™•์ธ +4. ์žฅ์•  ๋…ธ๋“œ ์ œ๊ฑฐ ๋ฐ ์ƒˆ ๋…ธ๋“œ ์ถ”๊ฐ€ (Auto Scaling) + +--- + +## 10. ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 10.1 ํŒจํ„ด ์ ์šฉ ์™„๋ฃŒ ํ™•์ธ + +- [ ] **Cache-Aside**: AI ๊ฒฐ๊ณผ, ์ด๋ฏธ์ง€, ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ ์บ์‹ฑ ์™„๋ฃŒ +- [ ] **API Gateway**: Nginx Ingress + JWT ์ธ์ฆ + Rate Limiting ์™„๋ฃŒ +- [ ] **Asynchronous Request-Reply**: RabbitMQ + Worker ๊ธฐ๋ฐ˜ Job ์ฒ˜๋ฆฌ ์™„๋ฃŒ +- [ ] **Circuit Breaker**: Resilience4j ์ ์šฉ ๋ฐ Fallback ์ „๋žต ๊ตฌํ˜„ ์™„๋ฃŒ + +### 10.2 ์„ฑ๋Šฅ ๋ชฉํ‘œ ๋‹ฌ์„ฑ ํ™•์ธ + +- [ ] AI ์‘๋‹ต ์‹œ๊ฐ„: 10์ดˆ ์ด๋‚ด (์บ์‹œ ๋ฏธ์Šค), 0.1์ดˆ (์บ์‹œ ํžˆํŠธ) +- [ ] ์บ์‹œ ํžˆํŠธ์œจ: 80% ์ด์ƒ +- [ ] ์‹œ์Šคํ…œ ๊ฐ€์šฉ์„ฑ: 99% ์ด์ƒ +- [ ] ๋™์‹œ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ ์š”์ฒญ: 50 req/sec ์ด์ƒ + +### 10.3 Azure ์ธํ”„๋ผ ๊ตฌ์ถ• ์™„๋ฃŒ ํ™•์ธ + +- [ ] AKS ํด๋Ÿฌ์Šคํ„ฐ ์ƒ์„ฑ (๊ฐœ๋ฐœ/์šด์˜) +- [ ] Azure Cache for Redis ๊ตฌ์„ฑ ๋ฐ ์—ฐ๊ฒฐ +- [ ] Azure Database for PostgreSQL ๊ตฌ์„ฑ ๋ฐ ์—ฐ๊ฒฐ +- [ ] Azure Container Registry ๊ตฌ์„ฑ ๋ฐ CI/CD ์—ฐ๋™ +- [ ] Nginx Ingress Controller ๋ฐฐํฌ ๋ฐ ์„ค์ • +- [ ] SSL ์ธ์ฆ์„œ ์ ์šฉ +- [ ] Azure Monitor + Application Insights ํ†ตํ•ฉ + +### 10.4 ์šด์˜ ์ค€๋น„ ์™„๋ฃŒ ํ™•์ธ + +- [ ] ๋ชจ๋‹ˆํ„ฐ๋ง ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์ถ• (Azure Monitor + Grafana) +- [ ] ์•Œ๋žŒ ์„ค์ • ์™„๋ฃŒ (Slack/Email) +- [ ] ์žฅ์•  ๋Œ€์‘ ๋งค๋‰ด์–ผ ์ž‘์„ฑ +- [ ] ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ์™„๋ฃŒ (k6, 100๋ช…) +- [ ] ์žฅ์•  ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ ์™„๋ฃŒ (Circuit Breaker, ์บ์‹œ ์žฅ์• ) + +--- + +## 11. ๋‹ค์Œ ๋‹จ๊ณ„ (Phase 2 ์ดํ›„) + +**MVP ์ดํ›„ ํ™•์žฅ ๊ณ„ํš** (์„ ํƒ ์‚ฌํ•ญ): + +- **Retry ํŒจํ„ด**: ์ผ์‹œ์  ์˜ค๋ฅ˜ ์ž๋™ ์žฌ์‹œ๋„ (ํ˜„์žฌ๋Š” Circuit Breaker๋กœ ์ปค๋ฒ„) +- **Queue-Based Load Leveling**: ํŠธ๋ž˜ํ”ฝ ํญ์ฆ ์‹œ ๋ถ€ํ•˜ ๋ถ„์‚ฐ +- **Saga ํŒจํ„ด**: ๋ณต์žกํ•œ ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ (์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ) +- **CQRS**: ์ฝ๊ธฐ/์“ฐ๊ธฐ ๋ถ„๋ฆฌ๋กœ ๋Œ€์‹œ๋ณด๋“œ ์„ฑ๋Šฅ ์ตœ์ ํ™” +- **Event Sourcing**: ์ด๋ฒคํŠธ ๋ณ€๊ฒฝ ์ด๋ ฅ ์ถ”์  ๋ฐ ๊ฐ์‚ฌ +- **Service Mesh (Istio)**: ๊ณ ๊ธ‰ ํŠธ๋ž˜ํ”ฝ ๊ด€๋ฆฌ ๋ฐ ๋ณด์•ˆ +- **Azure Front Door**: ๊ธ€๋กœ๋ฒŒ CDN ๋ฐ WAF +- **Azure Key Vault**: ์‹œํฌ๋ฆฟ ๊ด€๋ฆฌ ๊ฐ•ํ™” + +--- + +## ์ฐธ๊ณ  ๋ฌธ์„œ + +- [์œ ์ €์Šคํ† ๋ฆฌ](../userstory.md) +- [UI/UX ์„ค๊ณ„์„œ](../uiux/uiux.md) +- [ํด๋ผ์šฐ๋“œ ๋””์ž์ธ ํŒจํ„ด ๊ฐœ์š”](../../claude/cloud-design-patterns.md) +- [Azure Kubernetes Service ๊ณต์‹ ๋ฌธ์„œ](https://learn.microsoft.com/ko-kr/azure/aks/) +- [Azure Cache for Redis ๊ณต์‹ ๋ฌธ์„œ](https://learn.microsoft.com/ko-kr/azure/azure-cache-for-redis/) +- [Resilience4j ๊ณต์‹ ๋ฌธ์„œ](https://resilience4j.readme.io/) +- [๋ฐฑ์—… ํŒŒ์ผ](./backup/) - ์ด์ „ ๋ฒ„์ „ diff --git a/design/uiux/prototype/01-๋กœ๊ทธ์ธ.html b/design/uiux/prototype/01-๋กœ๊ทธ์ธ.html new file mode 100644 index 0000000..53e723a --- /dev/null +++ b/design/uiux/prototype/01-๋กœ๊ทธ์ธ.html @@ -0,0 +1,196 @@ + + + + + + ๋กœ๊ทธ์ธ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + +
+
+ +
+
+ celebration +
+

KT AI ์ด๋ฒคํŠธ

+

์†Œ์ƒ๊ณต์ธ์„ ์œ„ํ•œ ์Šค๋งˆํŠธ ๋งˆ์ผ€ํŒ…

+
+ + +
+
+ + + +
+ +
+ + + +
+ +
+ + +
+ + + + +
+ + +
+
+ ๋˜๋Š” +
+
+ + +
+ + +
+ + +
+

+ ํšŒ์›๊ฐ€์ž… ์‹œ ์ด์šฉ์•ฝ๊ด€ ๋ฐ ๊ฐœ์ธ์ •๋ณด์ฒ˜๋ฆฌ๋ฐฉ์นจ์— ๋™์˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. +

+
+
+
+ + + + + diff --git a/design/uiux/prototype/02-ํšŒ์›๊ฐ€์ž….html b/design/uiux/prototype/02-ํšŒ์›๊ฐ€์ž….html new file mode 100644 index 0000000..5889254 --- /dev/null +++ b/design/uiux/prototype/02-ํšŒ์›๊ฐ€์ž….html @@ -0,0 +1,400 @@ + + + + + + ํšŒ์›๊ฐ€์ž… - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + +
+ +
+
+ +

ํšŒ์›๊ฐ€์ž…

+
+
+ + +
+
+

1/3 ๋‹จ๊ณ„

+
+ +
+ + + + + + + + +
+
+ + + + + diff --git a/design/uiux/prototype/03-ํ”„๋กœํ•„.html b/design/uiux/prototype/03-ํ”„๋กœํ•„.html new file mode 100644 index 0000000..359df83 --- /dev/null +++ b/design/uiux/prototype/03-ํ”„๋กœํ•„.html @@ -0,0 +1,214 @@ + + + + + + ํ”„๋กœํ•„ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + +
+ + + +
+ +
+
+ person +
+

ํ™๊ธธ๋™

+

hong@example.com

+
+ + +
+

๊ธฐ๋ณธ ์ •๋ณด

+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+

๋งค์žฅ ์ •๋ณด

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+

๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ

+ +
+ + +
+ +
+ + + 8์ž ์ด์ƒ, ์˜๋ฌธ๊ณผ ์ˆซ์ž๋ฅผ ํฌํ•จํ•ด์ฃผ์„ธ์š” +
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/04-๋กœ๊ทธ์•„์›ƒํ™•์ธ.html b/design/uiux/prototype/04-๋กœ๊ทธ์•„์›ƒํ™•์ธ.html new file mode 100644 index 0000000..b6c1acd --- /dev/null +++ b/design/uiux/prototype/04-๋กœ๊ทธ์•„์›ƒํ™•์ธ.html @@ -0,0 +1,54 @@ + + + + + + ๋กœ๊ทธ์•„์›ƒ ํ™•์ธ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + + + + + + + + diff --git a/design/uiux/prototype/05-๋Œ€์‹œ๋ณด๋“œ.html b/design/uiux/prototype/05-๋Œ€์‹œ๋ณด๋“œ.html new file mode 100644 index 0000000..afd3a1a --- /dev/null +++ b/design/uiux/prototype/05-๋Œ€์‹œ๋ณด๋“œ.html @@ -0,0 +1,219 @@ + + + + + + ๋Œ€์‹œ๋ณด๋“œ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + +
+ + + +
+ +
+

์•ˆ๋…•ํ•˜์„ธ์š”, ์‚ฌ์šฉ์ž๋‹˜!

+

์˜ค๋Š˜๋„ ์„ฑ๊ณต์ ์ธ ์ด๋ฒคํŠธ๋ฅผ ์ค€๋น„ํ•ด๋ณด์„ธ์š”

+
+ + +
+
+
+
+
+
+
+ + +
+
+

๋น ๋ฅธ ์‹œ์ž‘

+
+
+ + +
+
+ + +
+
+

์ง„ํ–‰ ์ค‘์ธ ์ด๋ฒคํŠธ

+ + ์ „์ฒด๋ณด๊ธฐ + chevron_right + +
+
+ +
+
+ + +
+

์ตœ๊ทผ ํ™œ๋™

+
+
+ +
+
+
+
+ + +
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/06-์ด๋ฒคํŠธ๋ชฉ๋ก.html b/design/uiux/prototype/06-์ด๋ฒคํŠธ๋ชฉ๋ก.html new file mode 100644 index 0000000..3a9dfac --- /dev/null +++ b/design/uiux/prototype/06-์ด๋ฒคํŠธ๋ชฉ๋ก.html @@ -0,0 +1,323 @@ + + + + + + ์ด๋ฒคํŠธ ๋ชฉ๋ก - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + +
+ + + +
+ +
+
+
+ search + +
+
+
+ + +
+
+ filter_list + + +
+
+ + +
+
+

์ •๋ ฌ:

+ +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ.html b/design/uiux/prototype/07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ.html new file mode 100644 index 0000000..6fb6465 --- /dev/null +++ b/design/uiux/prototype/07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ.html @@ -0,0 +1,216 @@ + + + + + + ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + +
+ + + +
+ +
+
+ auto_awesome +
+

์ด๋ฒคํŠธ ๋ชฉ์ ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”

+

AI๊ฐ€ ๋ชฉ์ ์— ๋งž๋Š” ์ตœ์ ์˜ ์ด๋ฒคํŠธ๋ฅผ ์ถ”์ฒœํ•ด๋“œ๋ฆฝ๋‹ˆ๋‹ค

+
+ + +
+
+ +
+
+ + +
+ + +
+ + +
+
+
+ info +
+

+ ์„ ํƒํ•˜์‹  ๋ชฉ์ ์— ๋”ฐ๋ผ AI๊ฐ€ ์—…์ข…, ์ง€์—ญ, ๊ณ„์ ˆ ํŠธ๋ Œ๋“œ๋ฅผ ๋ถ„์„ํ•˜์—ฌ ๊ฐ€์žฅ ํšจ๊ณผ์ ์ธ ์ด๋ฒคํŠธ๋ฅผ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค. +

+
+
+
+
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/08-AI์ด๋ฒคํŠธ์ถ”์ฒœ.html b/design/uiux/prototype/08-AI์ด๋ฒคํŠธ์ถ”์ฒœ.html new file mode 100644 index 0000000..b4e8291 --- /dev/null +++ b/design/uiux/prototype/08-AI์ด๋ฒคํŠธ์ถ”์ฒœ.html @@ -0,0 +1,473 @@ + + + + + + AI ์ด๋ฒคํŠธ ์ถ”์ฒœ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + + +
+ + + +
+ +
+

+ insights + AI ํŠธ๋ Œ๋“œ ๋ถ„์„ +

+
+
+
+ store + ์—…์ข… ํŠธ๋ Œ๋“œ +
+

์Œ์‹์ ์—… ์‹ ๋…„ ํ”„๋กœ๋ชจ์…˜ ํŠธ๋ Œ๋“œ

+
+
+
+ location_on + ์ง€์—ญ ํŠธ๋ Œ๋“œ +
+

๊ฐ•๋‚จ๊ตฌ ์Œ์‹์  ํ• ์ธ ์ด๋ฒคํŠธ ์ฆ๊ฐ€

+
+
+
+ wb_sunny + ์‹œ์ฆŒ ํŠธ๋ Œ๋“œ +
+

์„ค ์—ฐํœด ํŠน์ˆ˜ ๋Œ€๋น„ ๊ณ ๊ฐ ์œ ์น˜ ์ „๋žต

+
+
+
+ + +
+

์˜ˆ์‚ฐ๋ณ„ ์ถ”์ฒœ ์ด๋ฒคํŠธ

+

+ ๊ฐ ์˜ˆ์‚ฐ๋ณ„ 2๊ฐ€์ง€ ๋ฐฉ์‹ (์˜จ๋ผ์ธ 1๊ฐœ, ์˜คํ”„๋ผ์ธ 1๊ฐœ)์„ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค +

+
+ + +
+
+ + + +
+
+ + +
+

๐Ÿ’ฐ ์˜ต์…˜ 1: ์ €๋น„์šฉ (25~30๋งŒ์›)

+
+
+
+ +
+
+ ๐ŸŒ ์˜จ๋ผ์ธ ๋ฐฉ์‹ +

+ SNS ํŒ”๋กœ์šฐ ์ด๋ฒคํŠธ + edit +

+
+
+ ๊ฒฝํ’ˆ: + ์ปคํ”ผ ์ฟ ํฐ + edit +
+
+
+ ์ฐธ์—ฌ ๋ฐฉ๋ฒ•: + SNS ํŒ”๋กœ์šฐ +
+
+ ์˜ˆ์ƒ ์ฐธ์—ฌ: + 180๋ช… +
+
+ ์˜ˆ์ƒ ๋น„์šฉ: + 25๋งŒ์› +
+
+ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : + 520% +
+
+
+ +
+
+ +
+
+ ๐Ÿช ์˜คํ”„๋ผ์ธ ๋ฐฉ์‹ +

+ ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก ์ด๋ฒคํŠธ + edit +

+
+
+ ๊ฒฝํ’ˆ: + ์ปคํ”ผ ์ฟ ํฐ + edit +
+
+
+ ์ฐธ์—ฌ ๋ฐฉ๋ฒ•: + ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก +
+
+ ์˜ˆ์ƒ ์ฐธ์—ฌ: + 150๋ช… +
+
+ ์˜ˆ์ƒ ๋น„์šฉ: + 30๋งŒ์› +
+
+ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : + 450% +
+
+
+
+
+ + +
+

๐Ÿ’ฐ๐Ÿ’ฐ ์˜ต์…˜ 2: ์ค‘๋น„์šฉ (150~180๋งŒ์›)

+
+
+
+ +
+
+ ๐ŸŒ ์˜จ๋ผ์ธ ๋ฐฉ์‹ +

+ ๋ฆฌ๋ทฐ ์ž‘์„ฑ ์ด๋ฒคํŠธ + edit +

+
+
+ ๊ฒฝํ’ˆ: + 5์ฒœ์› ์ƒํ’ˆ๊ถŒ + edit +
+
+
+ ์ฐธ์—ฌ ๋ฐฉ๋ฒ•: + ๋ฆฌ๋ทฐ ์ž‘์„ฑ +
+
+ ์˜ˆ์ƒ ์ฐธ์—ฌ: + 250๋ช… +
+
+ ์˜ˆ์ƒ ๋น„์šฉ: + 150๋งŒ์› +
+
+ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : + 380% +
+
+
+ +
+
+ +
+
+ ๐Ÿช ์˜คํ”„๋ผ์ธ ๋ฐฉ์‹ +

+ ๋ฐฉ๋ฌธ ๋„์žฅ ์ ๋ฆฝ ์ด๋ฒคํŠธ + edit +

+
+
+ ๊ฒฝํ’ˆ: + ๋ฌด๋ฃŒ ์‹์‚ฌ๊ถŒ + edit +
+
+
+ ์ฐธ์—ฌ ๋ฐฉ๋ฒ•: + ๋ฐฉ๋ฌธ ๋„์žฅ ์ ๋ฆฝ +
+
+ ์˜ˆ์ƒ ์ฐธ์—ฌ: + 200๋ช… +
+
+ ์˜ˆ์ƒ ๋น„์šฉ: + 180๋งŒ์› +
+
+ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : + 320% +
+
+
+
+
+ + +
+

๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ ์˜ต์…˜ 3: ๊ณ ๋น„์šฉ (500~600๋งŒ์›)

+
+
+
+ +
+
+ ๐ŸŒ ์˜จ๋ผ์ธ ๋ฐฉ์‹ +

+ ์ธํ”Œ๋ฃจ์–ธ์„œ ํ˜‘์—… ์ด๋ฒคํŠธ + edit +

+
+
+ ๊ฒฝํ’ˆ: + 1๋งŒ์› ํ• ์ธ๊ถŒ + edit +
+
+
+ ์ฐธ์—ฌ ๋ฐฉ๋ฒ•: + ์ธํ”Œ๋ฃจ์–ธ์„œ ํŒ”๋กœ์šฐ +
+
+ ์˜ˆ์ƒ ์ฐธ์—ฌ: + 400๋ช… +
+
+ ์˜ˆ์ƒ ๋น„์šฉ: + 500๋งŒ์› +
+
+ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : + 280% +
+
+
+ +
+
+ +
+
+ ๐Ÿช ์˜คํ”„๋ผ์ธ ๋ฐฉ์‹ +

+ VIP ๊ณ ๊ฐ ์ดˆ๋Œ€ ์ด๋ฒคํŠธ + edit +

+
+
+ ๊ฒฝํ’ˆ: + ํŠน๋ณ„ ๋ฉ”๋‰ด ์ œ๊ณต + edit +
+
+
+ ์ฐธ์—ฌ ๋ฐฉ๋ฒ•: + VIP ์ดˆ๋Œ€์žฅ +
+
+ ์˜ˆ์ƒ ์ฐธ์—ฌ: + 300๋ช… +
+
+ ์˜ˆ์ƒ ๋น„์šฉ: + 600๋งŒ์› +
+
+ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : + 240% +
+
+
+
+
+ + +
+ + +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ.html b/design/uiux/prototype/09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ.html new file mode 100644 index 0000000..4cb5c02 --- /dev/null +++ b/design/uiux/prototype/09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ.html @@ -0,0 +1,447 @@ + + + + + + SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + + +
+ + + +
+ +
+ psychology +

AI ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘

+

+ ๋”ฅ๋Ÿฌ๋‹ ๋ชจ๋ธ์ด ์ด๋ฒคํŠธ์— ์–ด์šธ๋ฆฌ๋Š”
+ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์žˆ์–ด์š”... +

+

์˜ˆ์ƒ ์‹œ๊ฐ„: 5์ดˆ

+
+ + + +
+ + +
+
+ + +
+ + ์ด๋ฒคํŠธ ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ +
+ + + + + diff --git a/design/uiux/prototype/10-์ฝ˜ํ…์ธ ํŽธ์ง‘.html b/design/uiux/prototype/10-์ฝ˜ํ…์ธ ํŽธ์ง‘.html new file mode 100644 index 0000000..c14bf88 --- /dev/null +++ b/design/uiux/prototype/10-์ฝ˜ํ…์ธ ํŽธ์ง‘.html @@ -0,0 +1,209 @@ + + + + + + ์ฝ˜ํ…์ธ  ํŽธ์ง‘ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + + +
+ + + +
+ +
+ +
+

๋ฏธ๋ฆฌ๋ณด๊ธฐ

+
+
+ celebration +

์‹ ๊ทœ๊ณ ๊ฐ ์œ ์น˜ ์ด๋ฒคํŠธ

+

์ปคํ”ผ ์ฟ ํฐ 100๋งค

+

์ „ํ™”๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์ฐธ์—ฌํ•˜์„ธ์š”

+
+
+
+ + +
+

ํŽธ์ง‘

+ + +
+

+ edit + ํ…์ŠคํŠธ ํŽธ์ง‘ +

+ +
+ + + 11/50์ž +
+ +
+ + + 9/30์ž +
+ +
+ + + 17/100์ž +
+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/11-๋ฐฐํฌ์ฑ„๋„์„ ํƒ.html b/design/uiux/prototype/11-๋ฐฐํฌ์ฑ„๋„์„ ํƒ.html new file mode 100644 index 0000000..718dc3a --- /dev/null +++ b/design/uiux/prototype/11-๋ฐฐํฌ์ฑ„๋„์„ ํƒ.html @@ -0,0 +1,421 @@ + + + + + + ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + + +
+ + + +
+ +
+

๋ฐฐํฌ ์ฑ„๋„์„ ์„ ํƒํ•ด์ฃผ์„ธ์š”

+

(์ตœ์†Œ 1๊ฐœ ์ด์ƒ)

+
+ + +
+
+
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+

์˜ˆ์ƒ ๋…ธ์ถœ: 5๋งŒ๋ช…

+

๋น„์šฉ: 8๋งŒ์›

+
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+ + +
+ +
+

์—ฐ๊ฒฐ์Œ ์ž๋™ ์—…๋ฐ์ดํŠธ

+

์˜ˆ์ƒ ๋…ธ์ถœ: 3๋งŒ๋ช…

+

๋น„์šฉ: ๋ฌด๋ฃŒ

+
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+

์˜ˆ์ƒ ๋…ธ์ถœ: ๊ณ„์‚ฐ์ค‘...

+
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+

์˜ˆ์ƒ ๋…ธ์ถœ: -

+

๋น„์šฉ: ๋ฌด๋ฃŒ

+
+
+
+
+ + +
+
+
+ ์ด ์˜ˆ์ƒ ๋น„์šฉ + 0์› +
+
+ ์ด ์˜ˆ์ƒ ๋…ธ์ถœ + 0๋ช… +
+
+
+ + +
+ +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/12-์ตœ์ข…์Šน์ธ.html b/design/uiux/prototype/12-์ตœ์ข…์Šน์ธ.html new file mode 100644 index 0000000..b061730 --- /dev/null +++ b/design/uiux/prototype/12-์ตœ์ข…์Šน์ธ.html @@ -0,0 +1,367 @@ + + + + + + ์ตœ์ข… ์Šน์ธ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + +
+ + + +
+ +
+
+ check_circle +
+

์ด๋ฒคํŠธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”

+

+ ๋ชจ๋“  ์ •๋ณด๋ฅผ ๊ฒ€ํ† ํ•œ ํ›„ ๋ฐฐํฌํ•˜์„ธ์š” +

+
+ + +
+
+

+ SNS ํŒ”๋กœ์šฐ ์ด๋ฒคํŠธ +

+ +
+ ๋ฐฐํฌ ๋Œ€๊ธฐ + + AI ์ถ”์ฒœ + +
+ +
+
+
+

์ด๋ฒคํŠธ ๊ธฐ๊ฐ„

+

+ 2025.02.01 ~ 2025.02.28 +

+
+
+

๋ชฉํ‘œ ์ฐธ์—ฌ์ž

+

180๋ช…

+
+
+

์˜ˆ์ƒ ๋น„์šฉ

+

250,000์›

+
+
+

์˜ˆ์ƒ ROI

+

+ 520% +

+
+
+
+
+
+ + +
+

์ด๋ฒคํŠธ ์ƒ์„ธ

+ +
+
+ celebration +
+

์ด๋ฒคํŠธ ์ œ๋ชฉ

+

SNS ํŒ”๋กœ์šฐ ์ด๋ฒคํŠธ

+
+ +
+
+ +
+
+ card_giftcard +
+

๊ฒฝํ’ˆ

+

์ปคํ”ผ ์ฟ ํฐ

+
+ +
+
+ +
+
+ description +
+

์ด๋ฒคํŠธ ์„ค๋ช…

+

+ SNS๋ฅผ ํŒ”๋กœ์šฐํ•˜๊ณ  ์ปคํ”ผ ์ฟ ํฐ์„ ๋ฐ›์œผ์„ธ์š”!
+ ๋งŽ์€ ์ฐธ์—ฌ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค. +

+
+ +
+
+ +
+
+ how_to_reg +
+

์ฐธ์—ฌ ๋ฐฉ๋ฒ•

+

SNS ํŒ”๋กœ์šฐ

+
+
+
+
+ + +
+

๋ฐฐํฌ ์ฑ„๋„

+
+
+ + language + ํ™ˆํŽ˜์ด์ง€ + + + chat_bubble + ์นด์นด์˜คํ†ก + + + share + Instagram + +
+ +
+
+ + +
+
+
+ + +
+ ์•ฝ๊ด€ ๋ณด๊ธฐ +
+
+ + +
+ + +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/13-์ด๋ฒคํŠธ์ƒ์„ธ.html b/design/uiux/prototype/13-์ด๋ฒคํŠธ์ƒ์„ธ.html new file mode 100644 index 0000000..2df2ebb --- /dev/null +++ b/design/uiux/prototype/13-์ด๋ฒคํŠธ์ƒ์„ธ.html @@ -0,0 +1,437 @@ + + + + + + ์ด๋ฒคํŠธ ์ƒ์„ธ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + +
+ + + +
+ +
+
+

SNS ํŒ”๋กœ์šฐ ์ด๋ฒคํŠธ

+ +
+ +
+ ์ง„ํ–‰์ค‘ + + AI ์ถ”์ฒœ + +
+ +

+ 2025.01.15 ~ 2025.02.15 +

+
+ + +
+
+

์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ

+
+ fiber_manual_record + ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ +
+
+ +
+
+
+
+
+
+
+ + +
+

์ฐธ์—ฌ ์ถ”์ด

+
+
+
+ + + +
+
+ + +
+
+ show_chart +

์ฐธ์—ฌ์ž ์ถ”์ด ์ฐจํŠธ

+
+
+
+
+ + +
+

์ด๋ฒคํŠธ ์ •๋ณด

+ +
+
+ card_giftcard +
+

๊ฒฝํ’ˆ

+

์ปคํ”ผ ์ฟ ํฐ

+
+
+
+ +
+
+ how_to_reg +
+

์ฐธ์—ฌ ๋ฐฉ๋ฒ•

+

SNS ํŒ”๋กœ์šฐ

+
+
+
+ +
+
+ attach_money +
+

์˜ˆ์ƒ ๋น„์šฉ

+

250,000์›

+
+
+
+ +
+
+ share +
+

๋ฐฐํฌ ์ฑ„๋„

+
+ ํ™ˆํŽ˜์ด์ง€ + ์นด์นด์˜คํ†ก + Instagram +
+
+
+
+
+ + +
+

๋น ๋ฅธ ์ž‘์—…

+
+ + + + +
+
+ + +
+
+

์ตœ๊ทผ ์ฐธ์—ฌ์ž

+ + ์ „์ฒด๋ณด๊ธฐ + chevron_right + +
+ +
+
+ +
+
+
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/14-์ฐธ์—ฌ์ž๋ชฉ๋ก.html b/design/uiux/prototype/14-์ฐธ์—ฌ์ž๋ชฉ๋ก.html new file mode 100644 index 0000000..a2fc5f4 --- /dev/null +++ b/design/uiux/prototype/14-์ฐธ์—ฌ์ž๋ชฉ๋ก.html @@ -0,0 +1,400 @@ + + + + + + ์ฐธ์—ฌ์ž ๋ชฉ๋ก - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + +
+ + + +
+ +
+
+
+ search + +
+
+
+ + +
+
+ filter_list + + +
+
+ + +
+
+

์ด 128๋ช… ์ฐธ์—ฌ

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/15-์ด๋ฒคํŠธ์ฐธ์—ฌ.html b/design/uiux/prototype/15-์ด๋ฒคํŠธ์ฐธ์—ฌ.html new file mode 100644 index 0000000..603a92f --- /dev/null +++ b/design/uiux/prototype/15-์ด๋ฒคํŠธ์ฐธ์—ฌ.html @@ -0,0 +1,309 @@ + + + + + + ์ด๋ฒคํŠธ ์ฐธ์—ฌ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + + +
+ +
+ celebration +

์‹ ๊ทœ๊ณ ๊ฐ ์œ ์น˜ ์ด๋ฒคํŠธ

+
+ + +
+
+ card_giftcard +
+

๊ฒฝํ’ˆ

+

์ปคํ”ผ ์ฟ ํฐ

+
+
+ +
+ calendar_today +
+

๊ธฐ๊ฐ„

+

2025-11-01 ~ 2025-11-15

+
+
+
+ + +
+ + +
+

์ฐธ์—ฌํ•˜๊ธฐ

+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+

+ ์ฐธ์—ฌ์ž: 128๋ช… +

+
+
+ + +
+
+ check +
+

์ฐธ์—ฌ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

+

+ ํ™๊ธธ๋™๋‹˜์˜ ํ–‰์šด์„ ๊ธฐ์›ํ•ฉ๋‹ˆ๋‹ค! +

+ +
+

๋‹น์ฒจ์ž ๋ฐœํ‘œ

+

2025-11-16 (์›”)

+
+ + +
+ + + + + diff --git a/design/uiux/prototype/16-๋‹น์ฒจ์ž์ถ”์ฒจ.html b/design/uiux/prototype/16-๋‹น์ฒจ์ž์ถ”์ฒจ.html new file mode 100644 index 0000000..00749c1 --- /dev/null +++ b/design/uiux/prototype/16-๋‹น์ฒจ์ž์ถ”์ฒจ.html @@ -0,0 +1,536 @@ + + + + + + ๋‹น์ฒจ์ž ์ถ”์ฒจ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + + +
+ +
+ +
+
+
+ event_note +

์ด๋ฒคํŠธ ์ •๋ณด

+
+
+

์ด๋ฒคํŠธ๋ช…

+

์‹ ๊ทœ๊ณ ๊ฐ ์œ ์น˜ ์ด๋ฒคํŠธ

+
+
+

์ด ์ฐธ์—ฌ์ž

+

127๋ช…

+
+
+

์ถ”์ฒจ ์ƒํƒœ

+

์ถ”์ฒจ ์ „

+
+
+
+ + +
+
+
+ tune +

์ถ”์ฒจ ์„ค์ •

+
+ +
+ +
+ +
5
+ +
+
+ +
+ + +
+ +
+

+ info + ์ถ”์ฒจ ๋ฐฉ์‹ +

+

โ€ข ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฌด์ž‘์œ„ ์ถ”์ฒจ

+

โ€ข ๋ชจ๋“  ์ถ”์ฒจ ๊ณผ์ •์€ ์ž๋™ ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค

+
+
+
+ + +
+ +
+ + +
+

๐Ÿ“œ ์ถ”์ฒจ ์ด๋ ฅ (์ตœ๊ทผ 3๊ฑด)

+
+ +
+
+
+ + +
+ +
+

๐ŸŽ‰ ์ถ”์ฒจ ์™„๋ฃŒ!

+

์ด 127๋ช… ์ค‘ 5๋ช… ๋‹น์ฒจ

+
+ + +
+

๐Ÿ† ๋‹น์ฒจ์ž ๋ชฉ๋ก

+
+ +
+
+

๐ŸŒŸ ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์  ์ ์šฉ

+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+
+
+ + +
+
๐ŸŽฐ
+

์ถ”์ฒจ ์ค‘...

+

๋‚œ์ˆ˜ ์ƒ์„ฑ ์ค‘

+
+ + + + + diff --git a/design/uiux/prototype/17-์„ฑ๊ณผ๋ถ„์„.html b/design/uiux/prototype/17-์„ฑ๊ณผ๋ถ„์„.html new file mode 100644 index 0000000..77feecf --- /dev/null +++ b/design/uiux/prototype/17-์„ฑ๊ณผ๋ถ„์„.html @@ -0,0 +1,430 @@ + + + + + + ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ - KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… + + + + + + +
+ + + +
+ +
+

๐Ÿ“Š ์š”์•ฝ (์‹ค์‹œ๊ฐ„)

+
+
+ ๋ฐฉ๊ธˆ ์ „ +
+
+ + +
+
+
+

์ฐธ์—ฌ์ž ์ˆ˜

+
128๋ช…
+

โ†‘ 12๋ช… (์˜ค๋Š˜)

+
+
+

์ด ๋น„์šฉ

+
30๋งŒ์›
+

๊ฒฝํ’ˆ 25๋งŒ + ์ฑ„๋„ 5๋งŒ

+
+
+

์˜ˆ์ƒ ์ˆ˜์ต

+
135๋งŒ์›
+

๋งค์ถœ์ฆ๊ฐ€ 100๋งŒ + LTV 35๋งŒ

+
+
+

ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ 

+
450%
+

๋ชฉํ‘œ 300% ๋‹ฌ์„ฑ

+
+
+
+ + +
+ +
+
+
+ pie_chart +

์ฑ„๋„๋ณ„ ์„ฑ๊ณผ

+
+
+ donut_large +

ํŒŒ์ด ์ฐจํŠธ

+
+
+
+
+ ์šฐ๋ฆฌ๋™๋„คTV + 45% (58๋ช…) +
+
+
+ ๋ง๊ณ ๋น„์ฆˆ + 30% (38๋ช…) +
+
+
+ SNS + 25% (32๋ช…) +
+
+
+
+ + +
+
+
+ show_chart +

์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด

+
+
+ trending_up +

๋ผ์ธ ์ฐจํŠธ

+
+
+

ํ”ผํฌ ์‹œ๊ฐ„: ์˜คํ›„ 2-4์‹œ (35๋ช…)

+

ํ‰๊ท  ์‹œ๊ฐ„๋‹น: 8๋ช…

+
+
+
+
+ + +
+ +
+
+
+ payments +

ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  ์ƒ์„ธ

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
์ด ๋น„์šฉ: 30๋งŒ์›
โ€ข ๊ฒฝํ’ˆ ๋น„์šฉ25๋งŒ์›
โ€ข ์ฑ„๋„ ๋น„์šฉ5๋งŒ์›
์˜ˆ์ƒ ์ˆ˜์ต: 135๋งŒ์›
โ€ข ๋งค์ถœ ์ฆ๊ฐ€100๋งŒ์›
โ€ข ์‹ ๊ทœ ๊ณ ๊ฐ LTV35๋งŒ์›
ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ 
+
+

(์ˆ˜์ต - ๋น„์šฉ) รท ๋น„์šฉ ร— 100

+

(135๋งŒ - 30๋งŒ) รท 30๋งŒ ร— 100

+

= 450%

+
+
+
+
+ + +
+
+
+ people +

์ฐธ์—ฌ์ž ํ”„๋กœํ•„

+
+ +
+

์—ฐ๋ น๋ณ„

+
+
+ 20๋Œ€ +
+
35%
+
+
+
+ 30๋Œ€ +
+
40%
+
+
+
+ 40๋Œ€ +
+
25%
+
+
+
+
+ +
+

์„ฑ๋ณ„

+
+
+ ์—ฌ์„ฑ +
+
60%
+
+
+
+ ๋‚จ์„ฑ +
+
40%
+
+
+
+
+
+
+
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/common.js b/design/uiux/prototype/common.js new file mode 100644 index 0000000..0f796bd --- /dev/null +++ b/design/uiux/prototype/common.js @@ -0,0 +1,1071 @@ +// ================================================================= +// KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… ์„œ๋น„์Šค - ๊ณตํ†ต JavaScript ๋ชจ๋“ˆ +// Version: 1.0.0 +// ================================================================= + +const KTEventApp = (() => { + 'use strict'; + + // ================================================================= + // 1. Utility Functions + // ================================================================= + const Utils = { + /** + * ์ „ํ™”๋ฒˆํ˜ธ ํฌ๋งทํŒ… (010-1234-5678) + */ + formatPhoneNumber(phone) { + const cleaned = phone.replace(/\D/g, ''); + const match = cleaned.match(/^(\d{3})(\d{3,4})(\d{4})$/); + if (match) { + return `${match[1]}-${match[2]}-${match[3]}`; + } + return phone; + }, + + /** + * ์ด๋ฉ”์ผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + */ + validateEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); + }, + + /** + * ๋น„๋ฐ€๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ (8์ž ์ด์ƒ, ์˜๋ฌธ+์ˆซ์ž ํฌํ•จ) + */ + validatePassword(password) { + const hasLength = password.length >= 8; + const hasLetter = /[a-zA-Z]/.test(password); + const hasNumber = /\d/.test(password); + return hasLength && hasLetter && hasNumber; + }, + + /** + * ๋‚ ์งœ ํฌ๋งทํŒ… (YYYY.MM.DD) + */ + formatDate(date) { + if (typeof date === 'string') { + date = new Date(date); + } + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}.${month}.${day}`; + }, + + /** + * ๋‚ ์งœ ์‹œ๊ฐ„ ํฌ๋งทํŒ… (YYYY.MM.DD HH:MM) + */ + formatDateTime(date) { + if (typeof date === 'string') { + date = new Date(date); + } + const dateStr = this.formatDate(date); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${dateStr} ${hours}:${minutes}`; + }, + + /** + * ์ˆซ์ž ํฌ๋งทํŒ… (1,234,567) + */ + formatNumber(num) { + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + }, + + /** + * ๊ธˆ์•ก ํฌ๋งทํŒ… (1,234,567์›) + */ + formatCurrency(amount) { + return `${this.formatNumber(amount)}์›`; + }, + + /** + * ๋””๋ฐ”์šด์Šค ํ•จ์ˆ˜ + */ + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + /** + * LocalStorage ์ €์žฅ + */ + saveToStorage(key, value) { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (e) { + console.error('Storage save failed:', e); + return false; + } + }, + + /** + * LocalStorage ์ฝ๊ธฐ + */ + getFromStorage(key, defaultValue = null) { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; + } catch (e) { + console.error('Storage read failed:', e); + return defaultValue; + } + }, + + /** + * LocalStorage ์‚ญ์ œ + */ + removeFromStorage(key) { + try { + localStorage.removeItem(key); + return true; + } catch (e) { + console.error('Storage remove failed:', e); + return false; + } + }, + + /** + * ๋žœ๋ค ID ์ƒ์„ฑ + */ + generateId() { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } + }; + + // ================================================================= + // 2. Navigation Components + // ================================================================= + const Navigation = { + /** + * ํ—ค๋” ์ƒ์„ฑ + */ + createHeader({ title = '', showBack = true, showMenu = true, showProfile = true } = {}) { + const header = document.createElement('header'); + header.className = 'header'; + + const leftDiv = document.createElement('div'); + leftDiv.className = 'header-left'; + + if (showMenu) { + const menuBtn = document.createElement('button'); + menuBtn.className = 'header-icon-btn'; + menuBtn.innerHTML = 'menu'; + menuBtn.setAttribute('aria-label', '๋ฉ”๋‰ด'); + leftDiv.appendChild(menuBtn); + } + + if (showBack) { + const backBtn = document.createElement('button'); + backBtn.className = 'header-icon-btn'; + backBtn.innerHTML = 'arrow_back'; + backBtn.setAttribute('aria-label', '๋’ค๋กœ๊ฐ€๊ธฐ'); + backBtn.addEventListener('click', () => window.history.back()); + leftDiv.appendChild(backBtn); + } + + const titleEl = document.createElement('h1'); + titleEl.className = 'header-title'; + titleEl.textContent = title; + leftDiv.appendChild(titleEl); + + const rightDiv = document.createElement('div'); + rightDiv.className = 'header-right'; + + if (showProfile) { + const profileBtn = document.createElement('button'); + profileBtn.className = 'header-icon-btn'; + profileBtn.innerHTML = 'account_circle'; + profileBtn.setAttribute('aria-label', 'ํ”„๋กœํ•„'); + rightDiv.appendChild(profileBtn); + } + + header.appendChild(leftDiv); + header.appendChild(rightDiv); + + return header; + }, + + /** + * ํ•˜๋‹จ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ƒ์„ฑ + */ + createBottomNav(currentPage = 'home') { + const nav = document.createElement('nav'); + nav.className = 'bottom-nav'; + nav.setAttribute('role', 'navigation'); + nav.setAttribute('aria-label', '์ฃผ ๋„ค๋น„๊ฒŒ์ด์…˜'); + + const navItems = [ + { id: 'home', label: 'ํ™ˆ', icon: 'home', href: '05-๋Œ€์‹œ๋ณด๋“œ.html' }, + { id: 'events', label: '์ด๋ฒคํŠธ', icon: 'celebration', href: '06-์ด๋ฒคํŠธ๋ชฉ๋ก.html' }, + { id: 'analytics', label: '๋ถ„์„', icon: 'analytics', href: '17-์„ฑ๊ณผ๋ถ„์„.html' }, + { id: 'profile', label: 'ํ”„๋กœํ•„', icon: 'person', href: '03-ํ”„๋กœํ•„.html' } + ]; + + navItems.forEach(item => { + const link = document.createElement('a'); + link.className = 'bottom-nav-item'; + if (item.id === currentPage) { + link.classList.add('active'); + } + link.href = item.href; + link.innerHTML = ` + ${item.icon} + ${item.label} + `; + nav.appendChild(link); + }); + + return nav; + }, + + /** + * Floating Action Button ์ƒ์„ฑ + */ + createFAB(icon = 'add', onClick) { + const fab = document.createElement('button'); + fab.className = 'fab'; + fab.innerHTML = `${icon}`; + fab.setAttribute('aria-label', '์ƒˆ ์ด๋ฒคํŠธ ์ƒ์„ฑ'); + + if (onClick) { + fab.addEventListener('click', onClick); + } + + return fab; + } + }; + + // ================================================================= + // 3. Form Components + // ================================================================= + const Form = { + /** + * ์ž…๋ ฅ ํ•„๋“œ ์ƒ์„ฑ + */ + createInput({ + type = 'text', + id, + name, + label, + placeholder = '', + required = false, + value = '', + error = '', + hint = '', + disabled = false, + onChange + } = {}) { + const group = document.createElement('div'); + group.className = 'form-group'; + + if (label) { + const labelEl = document.createElement('label'); + labelEl.className = 'form-label'; + if (required) { + labelEl.classList.add('form-label-required'); + } + labelEl.setAttribute('for', id); + labelEl.textContent = label; + group.appendChild(labelEl); + } + + const input = document.createElement('input'); + input.type = type; + input.id = id; + input.name = name || id; + input.className = 'form-input'; + input.placeholder = placeholder; + input.value = value; + input.disabled = disabled; + + if (required) { + input.required = true; + } + + if (onChange) { + input.addEventListener('input', onChange); + } + + group.appendChild(input); + + if (error) { + const errorEl = document.createElement('span'); + errorEl.className = 'form-error'; + errorEl.textContent = error; + group.appendChild(errorEl); + } + + if (hint) { + const hintEl = document.createElement('span'); + hintEl.className = 'form-hint'; + hintEl.textContent = hint; + group.appendChild(hintEl); + } + + return { group, input }; + }, + + /** + * ์„ ํƒ ํ•„๋“œ ์ƒ์„ฑ + */ + createSelect({ + id, + name, + label, + options = [], + required = false, + value = '', + onChange + } = {}) { + const group = document.createElement('div'); + group.className = 'form-group'; + + if (label) { + const labelEl = document.createElement('label'); + labelEl.className = 'form-label'; + if (required) { + labelEl.classList.add('form-label-required'); + } + labelEl.setAttribute('for', id); + labelEl.textContent = label; + group.appendChild(labelEl); + } + + const select = document.createElement('select'); + select.id = id; + select.name = name || id; + select.className = 'form-select'; + + if (required) { + select.required = true; + } + + options.forEach(opt => { + const option = document.createElement('option'); + option.value = opt.value; + option.textContent = opt.label; + if (opt.value === value) { + option.selected = true; + } + select.appendChild(option); + }); + + if (onChange) { + select.addEventListener('change', onChange); + } + + group.appendChild(select); + + return { group, select }; + }, + + /** + * ํ…์ŠคํŠธ ์˜์—ญ ์ƒ์„ฑ + */ + createTextarea({ + id, + name, + label, + placeholder = '', + required = false, + value = '', + rows = 4, + onChange + } = {}) { + const group = document.createElement('div'); + group.className = 'form-group'; + + if (label) { + const labelEl = document.createElement('label'); + labelEl.className = 'form-label'; + if (required) { + labelEl.classList.add('form-label-required'); + } + labelEl.setAttribute('for', id); + labelEl.textContent = label; + group.appendChild(labelEl); + } + + const textarea = document.createElement('textarea'); + textarea.id = id; + textarea.name = name || id; + textarea.className = 'form-textarea'; + textarea.placeholder = placeholder; + textarea.value = value; + textarea.rows = rows; + + if (required) { + textarea.required = true; + } + + if (onChange) { + textarea.addEventListener('input', onChange); + } + + group.appendChild(textarea); + + return { group, textarea }; + }, + + /** + * ์ฒดํฌ๋ฐ•์Šค ์ƒ์„ฑ + */ + createCheckbox({ + id, + name, + label, + checked = false, + onChange + } = {}) { + const checkDiv = document.createElement('div'); + checkDiv.className = 'form-check'; + + const input = document.createElement('input'); + input.type = 'checkbox'; + input.id = id; + input.name = name || id; + input.className = 'form-check-input'; + input.checked = checked; + + if (onChange) { + input.addEventListener('change', onChange); + } + + const labelEl = document.createElement('label'); + labelEl.className = 'form-check-label'; + labelEl.setAttribute('for', id); + labelEl.textContent = label; + + checkDiv.appendChild(input); + checkDiv.appendChild(labelEl); + + return { checkDiv, input }; + }, + + /** + * ๋ผ๋””์˜ค ๋ฒ„ํŠผ ์ƒ์„ฑ + */ + createRadio({ + id, + name, + value, + label, + checked = false, + onChange + } = {}) { + const radioDiv = document.createElement('div'); + radioDiv.className = 'form-check'; + + const input = document.createElement('input'); + input.type = 'radio'; + input.id = id; + input.name = name; + input.value = value; + input.className = 'form-check-input'; + input.checked = checked; + + if (onChange) { + input.addEventListener('change', onChange); + } + + const labelEl = document.createElement('label'); + labelEl.className = 'form-check-label'; + labelEl.setAttribute('for', id); + labelEl.textContent = label; + + radioDiv.appendChild(input); + radioDiv.appendChild(labelEl); + + return { radioDiv, input }; + }, + + /** + * ๋ฒ„ํŠผ ์ƒ์„ฑ + */ + createButton({ + text, + variant = 'primary', + size = 'large', + icon = null, + fullWidth = false, + disabled = false, + onClick + } = {}) { + const button = document.createElement('button'); + button.className = `btn btn-${variant} btn-${size}`; + + if (fullWidth) { + button.classList.add('btn-full'); + } + + button.disabled = disabled; + + if (icon) { + const iconEl = document.createElement('span'); + iconEl.className = 'material-icons'; + iconEl.textContent = icon; + button.appendChild(iconEl); + } + + const textNode = document.createTextNode(text); + button.appendChild(textNode); + + if (onClick) { + button.addEventListener('click', onClick); + } + + return button; + } + }; + + // ================================================================= + // 4. Feedback Components + // ================================================================= + const Feedback = { + /** + * Toast ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + */ + showToast(message, duration = 3000) { + const toast = document.createElement('div'); + toast.className = 'toast'; + toast.textContent = message; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.remove(); + }, duration); + }, + + /** + * Modal ํ‘œ์‹œ + */ + showModal({ title, content, buttons = [] } = {}) { + const backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop'; + + const modal = document.createElement('div'); + modal.className = 'modal'; + + if (title) { + const header = document.createElement('div'); + header.className = 'modal-header'; + const titleEl = document.createElement('h2'); + titleEl.className = 'modal-title'; + titleEl.textContent = title; + header.appendChild(titleEl); + modal.appendChild(header); + } + + if (content) { + const body = document.createElement('div'); + body.className = 'modal-body'; + if (typeof content === 'string') { + body.innerHTML = content; + } else { + body.appendChild(content); + } + modal.appendChild(body); + } + + if (buttons.length > 0) { + const footer = document.createElement('div'); + footer.className = 'modal-footer'; + + buttons.forEach(btnConfig => { + const btn = Form.createButton(btnConfig); + footer.appendChild(btn); + }); + + modal.appendChild(footer); + } + + const close = () => { + backdrop.remove(); + }; + + backdrop.addEventListener('click', (e) => { + if (e.target === backdrop) { + close(); + } + }); + + backdrop.appendChild(modal); + document.body.appendChild(backdrop); + + return { backdrop, modal, close }; + }, + + /** + * Bottom Sheet ํ‘œ์‹œ + */ + showBottomSheet(content) { + const backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop'; + + const sheet = document.createElement('div'); + sheet.className = 'bottom-sheet'; + + const handle = document.createElement('div'); + handle.className = 'bottom-sheet-handle'; + sheet.appendChild(handle); + + const sheetContent = document.createElement('div'); + sheetContent.className = 'bottom-sheet-content'; + + if (typeof content === 'string') { + sheetContent.innerHTML = content; + } else { + sheetContent.appendChild(content); + } + + sheet.appendChild(sheetContent); + + const close = () => { + backdrop.remove(); + }; + + backdrop.addEventListener('click', (e) => { + if (e.target === backdrop) { + close(); + } + }); + + backdrop.appendChild(sheet); + document.body.appendChild(backdrop); + + return { backdrop, sheet, close }; + }, + + /** + * Loading Spinner ํ‘œ์‹œ + */ + showSpinner(message = '') { + const container = document.createElement('div'); + container.className = 'spinner-center'; + container.innerHTML = ` +
+
+ ${message ? `

${message}

` : ''} +
+ `; + return container; + }, + + /** + * Progress Bar ์ƒ์„ฑ + */ + createProgressBar(value = 0) { + const progress = document.createElement('div'); + progress.className = 'progress'; + + const bar = document.createElement('div'); + bar.className = 'progress-bar'; + bar.style.width = `${value}%`; + bar.setAttribute('role', 'progressbar'); + bar.setAttribute('aria-valuenow', value); + bar.setAttribute('aria-valuemin', '0'); + bar.setAttribute('aria-valuemax', '100'); + + progress.appendChild(bar); + + return { + element: progress, + setValue: (newValue) => { + bar.style.width = `${newValue}%`; + bar.setAttribute('aria-valuenow', newValue); + } + }; + } + }; + + // ================================================================= + // 5. Card Components + // ================================================================= + const Cards = { + /** + * ์ด๋ฒคํŠธ ์นด๋“œ ์ƒ์„ฑ + */ + createEventCard({ + id, + title, + status, + startDate, + endDate, + participants, + views, + roi, + onClick + } = {}) { + const card = document.createElement('div'); + card.className = 'card event-card'; + + if (onClick) { + card.classList.add('card-clickable'); + card.addEventListener('click', () => onClick(id)); + } + + const statusBadgeClass = status === '์ง„ํ–‰์ค‘' ? 'badge-active' : + status === '์˜ˆ์ •' ? 'badge-scheduled' : + 'badge-ended'; + + card.innerHTML = ` +
+

${title}

+ ${status} +
+

+ ${Utils.formatDate(startDate)} ~ ${Utils.formatDate(endDate)} +

+
+
+
์ฐธ์—ฌ์ž
+
${Utils.formatNumber(participants)}
+
+
+
์กฐํšŒ์ˆ˜
+
${Utils.formatNumber(views)}
+
+
+
ROI
+
${roi}%
+
+
+ `; + + return card; + }, + + /** + * KPI ์นด๋“œ ์ƒ์„ฑ + */ + createKPICard({ + icon, + iconType = 'primary', + label, + value, + onClick + } = {}) { + const card = document.createElement('div'); + card.className = 'card kpi-card'; + + if (onClick) { + card.classList.add('card-clickable'); + card.addEventListener('click', onClick); + } + + card.innerHTML = ` +
+ ${icon} +
+
+
${label}
+
${value}
+
+ `; + + return card; + }, + + /** + * ์˜ต์…˜ ์„ ํƒ ์นด๋“œ ์ƒ์„ฑ + */ + createOptionCard({ + id, + title, + description, + content, + selected = false, + onSelect + } = {}) { + const card = document.createElement('div'); + card.className = 'card option-card'; + + if (selected) { + card.classList.add('selected'); + } + + card.innerHTML = ` + +

${title}

+ ${description ? `

${description}

` : ''} + ${content || ''} + `; + + card.addEventListener('click', () => { + document.querySelectorAll('.option-card').forEach(c => c.classList.remove('selected')); + card.classList.add('selected'); + const radio = card.querySelector('input[type="radio"]'); + radio.checked = true; + + if (onSelect) { + onSelect(id); + } + }); + + return card; + } + }; + + // ================================================================= + // 6. Session Management + // ================================================================= + const Session = { + /** + * ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ + */ + saveUser(user) { + return Utils.saveToStorage('kt_event_user', user); + }, + + /** + * ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ + */ + getUser() { + return Utils.getFromStorage('kt_event_user'); + }, + + /** + * ๋กœ๊ทธ์ธ ์—ฌ๋ถ€ ํ™•์ธ + */ + isLoggedIn() { + return this.getUser() !== null; + }, + + /** + * ๋กœ๊ทธ์•„์›ƒ + */ + logout() { + Utils.removeFromStorage('kt_event_user'); + window.location.href = '01-๋กœ๊ทธ์ธ.html'; + }, + + /** + * ๋กœ๊ทธ์ธ ํ•„์š” ํŽ˜์ด์ง€ ๋ณดํ˜ธ + */ + requireAuth() { + if (!this.isLoggedIn()) { + window.location.href = '01-๋กœ๊ทธ์ธ.html'; + } + } + }; + + // ================================================================= + // 7. Mock Data + // ================================================================= + const MockData = { + /** + * ์˜ˆ์ œ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ + */ + getEvents() { + return [ + { + id: 'evt001', + title: 'SNS ํŒ”๋กœ์šฐ ์ด๋ฒคํŠธ', + status: '์ง„ํ–‰์ค‘', + purpose: '์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜', + startDate: '2025-01-15', + endDate: '2025-02-15', + participants: 142, + views: 856, + roi: 520, + budget: '์ €๋น„์šฉ', + channel: '์˜จ๋ผ์ธ', + prize: '์ปคํ”ผ ์ฟ ํฐ' + }, + { + id: 'evt002', + title: '์„ค ๋งž์ด ํ• ์ธ ์ด๋ฒคํŠธ', + status: '์˜ˆ์ •', + purpose: '๋งค์ถœ ์ฆ๋Œ€', + startDate: '2025-02-01', + endDate: '2025-02-10', + participants: 0, + views: 0, + roi: 0, + budget: '์ค‘๋น„์šฉ', + channel: '์˜คํ”„๋ผ์ธ', + prize: '10% ํ• ์ธ๊ถŒ' + }, + { + id: 'evt003', + title: '๊ณ ๊ฐ ๋งŒ์กฑ๋„ ์กฐ์‚ฌ', + status: '์ข…๋ฃŒ', + purpose: '๊ณ ๊ฐ ์œ ์ง€', + startDate: '2024-12-01', + endDate: '2024-12-31', + participants: 287, + views: 1234, + roi: 340, + budget: '์ €๋น„์šฉ', + channel: '์˜จ๋ผ์ธ', + prize: 'ํฌ์ธํŠธ ์ ๋ฆฝ' + } + ]; + }, + + /** + * ์˜ˆ์ œ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ + */ + getDefaultUser() { + return { + id: 'user001', + name: 'ํ™๊ธธ๋™', + email: 'hong@example.com', + phone: '010-1234-5678', + businessName: 'ํ™๊ธธ๋™ ๊ณ ๊นƒ์ง‘', + businessType: '์Œ์‹์ ', + joinDate: '2025-01-01' + }; + }, + + /** + * ์ด๋ฒคํŠธ ๋ชฉ์  ์˜ต์…˜ + */ + getEventPurposes() { + return [ + { + id: 'new_customers', + title: '์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜', + description: '์ƒˆ๋กœ์šด ๊ณ ๊ฐ์„ ๋งค์žฅ์œผ๋กœ ๋Œ์–ด๋“ค์ด๊ณ  ์‹ถ์–ด์š”', + icon: 'person_add' + }, + { + id: 'sales', + title: '๋งค์ถœ ์ฆ๋Œ€', + description: '๋‹จ๊ธฐ๊ฐ„์— ๋งค์ถœ์„ ์˜ฌ๋ฆฌ๊ณ  ์‹ถ์–ด์š”', + icon: 'trending_up' + }, + { + id: 'retention', + title: '๊ณ ๊ฐ ์œ ์ง€', + description: '๊ธฐ์กด ๊ณ ๊ฐ์ด ๊ณ„์† ๋ฐฉ๋ฌธํ•˜๋„๋ก ํ•˜๊ณ  ์‹ถ์–ด์š”', + icon: 'favorite' + }, + { + id: 'awareness', + title: '๋ธŒ๋žœ๋“œ ์ธ์ง€๋„', + description: '์šฐ๋ฆฌ ๊ฐ€๊ฒŒ๋ฅผ ๋” ๋งŽ์€ ์‚ฌ๋žŒ์—๊ฒŒ ์•Œ๋ฆฌ๊ณ  ์‹ถ์–ด์š”', + icon: 'campaign' + } + ]; + }, + + /** + * AI ์ถ”์ฒœ ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ + */ + getAIRecommendations() { + return { + trends: { + industry: '์Œ์‹์ ์—… ์‹ ๋…„ ํ”„๋กœ๋ชจ์…˜ ํŠธ๋ Œ๋“œ', + location: '๊ฐ•๋‚จ๊ตฌ ์Œ์‹์  ํ• ์ธ ์ด๋ฒคํŠธ ์ฆ๊ฐ€', + season: '์„ค ์—ฐํœด ํŠน์ˆ˜ ๋Œ€๋น„ ๊ณ ๊ฐ ์œ ์น˜ ์ „๋žต' + }, + recommendations: [ + // ์ €๋น„์šฉ + { + budget: '์ €๋น„์šฉ', + type: '์˜จ๋ผ์ธ', + title: 'SNS ํŒ”๋กœ์šฐ ์ด๋ฒคํŠธ', + prize: '์ปคํ”ผ ์ฟ ํฐ', + method: 'SNS ํŒ”๋กœ์šฐ', + participants: 180, + cost: 250000, + roi: 520 + }, + { + budget: '์ €๋น„์šฉ', + type: '์˜คํ”„๋ผ์ธ', + title: '์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก ์ด๋ฒคํŠธ', + prize: '์ปคํ”ผ ์ฟ ํฐ', + method: '์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก', + participants: 150, + cost: 300000, + roi: 450 + }, + // ์ค‘๋น„์šฉ + { + budget: '์ค‘๋น„์šฉ', + type: '์˜จ๋ผ์ธ', + title: '๋ฆฌ๋ทฐ ์ž‘์„ฑ ์ด๋ฒคํŠธ', + prize: '5์ฒœ์› ์ƒํ’ˆ๊ถŒ', + method: '๋ฆฌ๋ทฐ ์ž‘์„ฑ', + participants: 250, + cost: 1500000, + roi: 380 + }, + { + budget: '์ค‘๋น„์šฉ', + type: '์˜คํ”„๋ผ์ธ', + title: '๋ฐฉ๋ฌธ ๋„์žฅ ์ ๋ฆฝ ์ด๋ฒคํŠธ', + prize: '๋ฌด๋ฃŒ ์‹์‚ฌ๊ถŒ', + method: '๋ฐฉ๋ฌธ ๋„์žฅ ์ ๋ฆฝ', + participants: 200, + cost: 1800000, + roi: 320 + }, + // ๊ณ ๋น„์šฉ + { + budget: '๊ณ ๋น„์šฉ', + type: '์˜จ๋ผ์ธ', + title: '์ธํ”Œ๋ฃจ์–ธ์„œ ํ˜‘์—… ์ด๋ฒคํŠธ', + prize: '1๋งŒ์› ํ• ์ธ๊ถŒ', + method: '์ธํ”Œ๋ฃจ์–ธ์„œ ํŒ”๋กœ์šฐ', + participants: 400, + cost: 5000000, + roi: 280 + }, + { + budget: '๊ณ ๋น„์šฉ', + type: '์˜คํ”„๋ผ์ธ', + title: 'VIP ๊ณ ๊ฐ ์ดˆ๋Œ€ ์ด๋ฒคํŠธ', + prize: 'ํŠน๋ณ„ ๋ฉ”๋‰ด ์ œ๊ณต', + method: 'VIP ์ดˆ๋Œ€์žฅ', + participants: 300, + cost: 6000000, + roi: 240 + } + ] + }; + } + }; + + // ================================================================= + // Public API + // ================================================================= + return { + Utils, + Navigation, + Form, + Feedback, + Cards, + Session, + MockData + }; +})(); + +// Material Icons ํฐํŠธ ๋กœ๋“œ +if (!document.querySelector('link[href*="material-icons"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://fonts.googleapis.com/icon?family=Material+Icons'; + document.head.appendChild(link); +} + +// Pretendard ํฐํŠธ ๋กœ๋“œ +if (!document.querySelector('link[href*="pretendard"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css'; + document.head.appendChild(link); +} diff --git a/design/uiux/prototype/styles.css b/design/uiux/prototype/styles.css new file mode 100644 index 0000000..0be54e8 --- /dev/null +++ b/design/uiux/prototype/styles.css @@ -0,0 +1,973 @@ +/* ================================================================= + KT AI ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… ์„œ๋น„์Šค - ๊ณตํ†ต ์Šคํƒ€์ผ์‹œํŠธ + Version: 1.0.0 + Mobile First Design Philosophy + ================================================================= */ + +/* ================================================================= + 1. CSS Variables - Design System + ================================================================= */ +:root { + /* Brand Colors */ + --color-kt-red: #E31E24; + --color-ai-blue: #0066FF; + + /* Grayscale */ + --color-gray-50: #F9FAFB; + --color-gray-100: #F3F4F6; + --color-gray-200: #E5E7EB; + --color-gray-300: #D1D5DB; + --color-gray-400: #9CA3AF; + --color-gray-500: #6B7280; + --color-gray-600: #4B5563; + --color-gray-700: #374151; + --color-gray-800: #1F2937; + --color-gray-900: #111827; + + /* Semantic Colors */ + --color-success: #10B981; + --color-warning: #F59E0B; + --color-error: #EF4444; + --color-info: #3B82F6; + + /* Background */ + --color-bg-primary: #FFFFFF; + --color-bg-secondary: #F9FAFB; + --color-bg-tertiary: #F3F4F6; + + /* Text */ + --color-text-primary: #111827; + --color-text-secondary: #4B5563; + --color-text-tertiary: #9CA3AF; + --color-text-inverse: #FFFFFF; + + /* Gradients */ + --gradient-primary: linear-gradient(135deg, #E31E24 0%, #B71419 100%); + --gradient-ai: linear-gradient(135deg, #0066FF 0%, #0052CC 100%); + + /* Typography Scale */ + --font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + --font-size-display: 28px; + --font-size-title-large: 24px; + --font-size-title: 20px; + --font-size-headline: 18px; + --font-size-body-large: 16px; + --font-size-body: 14px; + --font-size-body-small: 13px; + --font-size-caption: 12px; + + --line-height-display: 1.2; + --line-height-title: 1.3; + --line-height-body: 1.5; + --line-height-caption: 1.4; + + --font-weight-bold: 700; + --font-weight-semibold: 600; + --font-weight-medium: 500; + --font-weight-regular: 400; + + /* Spacing System (4px grid) */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + --spacing-2xl: 48px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15); + + /* Border Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-full: 9999px; + + /* Z-Index */ + --z-dropdown: 1000; + --z-sticky: 1020; + --z-fixed: 1030; + --z-modal-backdrop: 1040; + --z-modal: 1050; + --z-popover: 1060; + --z-tooltip: 1070; + + /* Animation */ + --duration-instant: 0ms; + --duration-fast: 150ms; + --duration-normal: 300ms; + --duration-slow: 500ms; + + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Tablet Responsive Variables */ +@media (min-width: 768px) { + :root { + --font-size-display: 32px; + --font-size-title-large: 28px; + --font-size-title: 24px; + --font-size-headline: 20px; + } +} + +/* Desktop Responsive Variables */ +@media (min-width: 1024px) { + :root { + --font-size-display: 36px; + --font-size-title-large: 32px; + --font-size-title: 28px; + --font-size-headline: 22px; + } +} + +/* ================================================================= + 2. Reset & Base Styles + ================================================================= */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-family); + font-size: var(--font-size-body); + line-height: var(--line-height-body); + color: var(--color-text-primary); + background-color: var(--color-bg-primary); + overflow-x: hidden; +} + +/* ================================================================= + 3. Typography + ================================================================= */ +.text-display { + font-size: var(--font-size-display); + line-height: var(--line-height-display); + font-weight: var(--font-weight-bold); +} + +.text-title-large { + font-size: var(--font-size-title-large); + line-height: var(--line-height-title); + font-weight: var(--font-weight-bold); +} + +.text-title { + font-size: var(--font-size-title); + line-height: var(--line-height-title); + font-weight: var(--font-weight-semibold); +} + +.text-headline { + font-size: var(--font-size-headline); + line-height: var(--line-height-title); + font-weight: var(--font-weight-semibold); +} + +.text-body-large { + font-size: var(--font-size-body-large); + line-height: var(--line-height-body); + font-weight: var(--font-weight-regular); +} + +.text-body { + font-size: var(--font-size-body); + line-height: var(--line-height-body); + font-weight: var(--font-weight-regular); +} + +.text-body-small { + font-size: var(--font-size-body-small); + line-height: var(--line-height-body); + font-weight: var(--font-weight-regular); +} + +.text-caption { + font-size: var(--font-size-caption); + line-height: var(--line-height-caption); + font-weight: var(--font-weight-regular); +} + +.text-primary { color: var(--color-text-primary); } +.text-secondary { color: var(--color-text-secondary); } +.text-tertiary { color: var(--color-text-tertiary); } +.text-inverse { color: var(--color-text-inverse); } +.text-kt-red { color: var(--color-kt-red); } +.text-ai-blue { color: var(--color-ai-blue); } +.text-success { color: var(--color-success); } +.text-warning { color: var(--color-warning); } +.text-error { color: var(--color-error); } + +.text-bold { font-weight: var(--font-weight-bold); } +.text-semibold { font-weight: var(--font-weight-semibold); } +.text-medium { font-weight: var(--font-weight-medium); } + +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +/* ================================================================= + 4. Layout Utilities + ================================================================= */ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--spacing-md); +} + +.page { + min-height: 100vh; + background-color: var(--color-bg-secondary); + padding-bottom: 76px; /* Bottom nav height + spacing */ +} + +.page-with-header { + padding-top: 56px; /* Header height */ +} + +/* ================================================================= + 5. Button Components + ================================================================= */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + border: none; + border-radius: var(--radius-md); + font-family: var(--font-family); + font-weight: var(--font-weight-semibold); + cursor: pointer; + transition: all var(--duration-fast) var(--ease-out); + text-decoration: none; + user-select: none; +} + +.btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* Button Sizes */ +.btn-large { + height: 48px; + padding: 0 var(--spacing-lg); + font-size: var(--font-size-body-large); +} + +.btn-medium { + height: 44px; + padding: 0 var(--spacing-md); + font-size: var(--font-size-body); +} + +.btn-small { + height: 36px; + padding: 0 var(--spacing-md); + font-size: var(--font-size-body-small); +} + +/* Button Variants */ +.btn-primary { + background: var(--gradient-primary); + color: var(--color-text-inverse); + box-shadow: var(--shadow-sm); +} + +.btn-primary:hover:not(:disabled) { + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +.btn-primary:active:not(:disabled) { + transform: translateY(0); + box-shadow: var(--shadow-sm); +} + +.btn-secondary { + background-color: var(--color-bg-primary); + color: var(--color-kt-red); + border: 1px solid var(--color-gray-300); +} + +.btn-secondary:hover:not(:disabled) { + background-color: var(--color-gray-50); + border-color: var(--color-kt-red); +} + +.btn-text { + background-color: transparent; + color: var(--color-kt-red); +} + +.btn-text:hover:not(:disabled) { + background-color: rgba(227, 30, 36, 0.08); +} + +.btn-full { + width: 100%; +} + +/* ================================================================= + 6. Card Components + ================================================================= */ +.card { + background-color: var(--color-bg-primary); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + box-shadow: var(--shadow-sm); + transition: box-shadow var(--duration-fast) var(--ease-out); +} + +.card:hover { + box-shadow: var(--shadow-md); +} + +.card-clickable { + cursor: pointer; +} + +.card-clickable:active { + transform: scale(0.98); +} + +.event-card { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.event-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--spacing-sm); +} + +.event-card-badge { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: var(--radius-full); + font-size: var(--font-size-caption); + font-weight: var(--font-weight-medium); +} + +.badge-active { + background-color: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.badge-scheduled { + background-color: rgba(59, 130, 246, 0.1); + color: var(--color-info); +} + +.badge-ended { + background-color: rgba(156, 163, 175, 0.1); + color: var(--color-gray-500); +} + +.event-card-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--spacing-md); + padding-top: var(--spacing-md); + border-top: 1px solid var(--color-gray-200); +} + +.stat-item { + text-align: center; +} + +.stat-label { + font-size: var(--font-size-caption); + color: var(--color-text-tertiary); + margin-bottom: 4px; +} + +.stat-value { + font-size: var(--font-size-headline); + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); +} + +/* KPI Card */ +.kpi-card { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-md); +} + +.kpi-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + font-size: 24px; +} + +.kpi-icon-primary { + background: rgba(227, 30, 36, 0.1); + color: var(--color-kt-red); +} + +.kpi-icon-ai { + background: rgba(0, 102, 255, 0.1); + color: var(--color-ai-blue); +} + +.kpi-icon-success { + background: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.kpi-content { + flex: 1; +} + +.kpi-label { + font-size: var(--font-size-body-small); + color: var(--color-text-secondary); + margin-bottom: 4px; +} + +.kpi-value { + font-size: var(--font-size-title); + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); +} + +/* Option Card for Selection */ +.option-card { + position: relative; + border: 2px solid var(--color-gray-200); + transition: all var(--duration-fast) var(--ease-out); +} + +.option-card:hover { + border-color: var(--color-kt-red); +} + +.option-card.selected { + border-color: var(--color-kt-red); + background-color: rgba(227, 30, 36, 0.02); +} + +.option-card-radio { + position: absolute; + top: var(--spacing-md); + right: var(--spacing-md); +} + +/* ================================================================= + 7. Form Components + ================================================================= */ +.form-group { + margin-bottom: var(--spacing-lg); +} + +.form-label { + display: block; + font-size: var(--font-size-body); + font-weight: var(--font-weight-medium); + color: var(--color-text-primary); + margin-bottom: var(--spacing-sm); +} + +.form-label-required::after { + content: " *"; + color: var(--color-error); +} + +.form-input, +.form-select, +.form-textarea { + width: 100%; + padding: 12px var(--spacing-md); + border: 1px solid var(--color-gray-300); + border-radius: var(--radius-md); + font-family: var(--font-family); + font-size: var(--font-size-body); + color: var(--color-text-primary); + background-color: var(--color-bg-primary); + transition: all var(--duration-fast) var(--ease-out); +} + +.form-input::placeholder, +.form-textarea::placeholder { + color: var(--color-text-tertiary); +} + +.form-input:focus, +.form-select:focus, +.form-textarea:focus { + outline: none; + border-color: var(--color-kt-red); + box-shadow: 0 0 0 3px rgba(227, 30, 36, 0.1); +} + +.form-input:disabled, +.form-select:disabled, +.form-textarea:disabled { + background-color: var(--color-gray-100); + cursor: not-allowed; +} + +.form-textarea { + min-height: 100px; + resize: vertical; +} + +.form-error { + display: block; + margin-top: var(--spacing-sm); + font-size: var(--font-size-body-small); + color: var(--color-error); +} + +.form-hint { + display: block; + margin-top: var(--spacing-sm); + font-size: var(--font-size-body-small); + color: var(--color-text-tertiary); +} + +/* Checkbox & Radio */ +.form-check { + display: flex; + align-items: center; + gap: var(--spacing-sm); + cursor: pointer; +} + +.form-check-input { + width: 20px; + height: 20px; + cursor: pointer; +} + +.form-check-label { + font-size: var(--font-size-body); + color: var(--color-text-primary); + cursor: pointer; +} + +/* ================================================================= + 8. Navigation Components + ================================================================= */ +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 56px; + background-color: var(--color-bg-primary); + border-bottom: 1px solid var(--color-gray-200); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--spacing-md); + z-index: var(--z-sticky); +} + +.header-left, +.header-right { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.header-title { + font-size: var(--font-size-headline); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); +} + +.header-icon-btn { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: var(--color-text-primary); + cursor: pointer; + border-radius: var(--radius-md); + transition: background-color var(--duration-fast) var(--ease-out); +} + +.header-icon-btn:hover { + background-color: var(--color-gray-100); +} + +.bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 60px; + background-color: var(--color-bg-primary); + border-top: 1px solid var(--color-gray-200); + display: flex; + align-items: center; + justify-content: space-around; + padding: 0 var(--spacing-sm); + z-index: var(--z-sticky); +} + +.bottom-nav-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + padding: var(--spacing-sm); + color: var(--color-text-tertiary); + text-decoration: none; + font-size: var(--font-size-caption); + transition: color var(--duration-fast) var(--ease-out); + cursor: pointer; +} + +.bottom-nav-item:hover { + color: var(--color-text-secondary); +} + +.bottom-nav-item.active { + color: var(--color-kt-red); +} + +.bottom-nav-icon { + font-size: 24px; +} + +/* FAB (Floating Action Button) */ +.fab { + position: fixed; + bottom: 76px; /* Bottom nav height + spacing */ + right: var(--spacing-md); + width: 56px; + height: 56px; + background: var(--gradient-primary); + color: var(--color-text-inverse); + border: none; + border-radius: var(--radius-full); + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + cursor: pointer; + transition: all var(--duration-fast) var(--ease-out); + z-index: var(--z-fixed); +} + +.fab:hover { + box-shadow: var(--shadow-xl); + transform: scale(1.05); +} + +.fab:active { + transform: scale(0.95); +} + +/* ================================================================= + 9. Feedback Components + ================================================================= */ +/* Toast */ +.toast { + position: fixed; + bottom: 92px; /* Bottom nav + spacing */ + left: 50%; + transform: translateX(-50%); + padding: var(--spacing-md) var(--spacing-lg); + background-color: var(--color-gray-900); + color: var(--color-text-inverse); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + font-size: var(--font-size-body); + z-index: var(--z-tooltip); + animation: slideUp var(--duration-normal) var(--ease-out); +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translate(-50%, 20px); + } + to { + opacity: 1; + transform: translate(-50%, 0); + } +} + +/* Modal */ +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: var(--z-modal-backdrop); + animation: fadeIn var(--duration-normal) var(--ease-out); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--color-bg-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + z-index: var(--z-modal); + max-width: 90%; + max-height: 90vh; + overflow-y: auto; + animation: scaleIn var(--duration-normal) var(--ease-out); +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +.modal-header { + padding: var(--spacing-lg); + border-bottom: 1px solid var(--color-gray-200); +} + +.modal-title { + font-size: var(--font-size-title); + font-weight: var(--font-weight-semibold); +} + +.modal-body { + padding: var(--spacing-lg); +} + +.modal-footer { + padding: var(--spacing-lg); + border-top: 1px solid var(--color-gray-200); + display: flex; + gap: var(--spacing-sm); + justify-content: flex-end; +} + +/* Bottom Sheet */ +.bottom-sheet { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: var(--color-bg-primary); + border-radius: var(--radius-xl) var(--radius-xl) 0 0; + box-shadow: var(--shadow-xl); + z-index: var(--z-modal); + max-height: 80vh; + overflow-y: auto; + animation: slideUpSheet var(--duration-normal) var(--ease-out); +} + +@keyframes slideUpSheet { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +.bottom-sheet-handle { + width: 40px; + height: 4px; + background-color: var(--color-gray-300); + border-radius: var(--radius-full); + margin: var(--spacing-sm) auto; +} + +.bottom-sheet-content { + padding: var(--spacing-lg); +} + +/* Spinner */ +.spinner { + width: 40px; + height: 40px; + border: 4px solid var(--color-gray-200); + border-top-color: var(--color-kt-red); + border-radius: var(--radius-full); + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.spinner-center { + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-2xl); +} + +/* Progress Bar */ +.progress { + width: 100%; + height: 8px; + background-color: var(--color-gray-200); + border-radius: var(--radius-full); + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: var(--gradient-primary); + border-radius: var(--radius-full); + transition: width var(--duration-normal) var(--ease-out); +} + +/* ================================================================= + 10. Utility Classes + ================================================================= */ +/* Spacing */ +.m-0 { margin: 0; } +.mt-xs { margin-top: var(--spacing-xs); } +.mt-sm { margin-top: var(--spacing-sm); } +.mt-md { margin-top: var(--spacing-md); } +.mt-lg { margin-top: var(--spacing-lg); } +.mt-xl { margin-top: var(--spacing-xl); } +.mt-2xl { margin-top: var(--spacing-2xl); } + +.mb-xs { margin-bottom: var(--spacing-xs); } +.mb-sm { margin-bottom: var(--spacing-sm); } +.mb-md { margin-bottom: var(--spacing-md); } +.mb-lg { margin-bottom: var(--spacing-lg); } +.mb-xl { margin-bottom: var(--spacing-xl); } +.mb-2xl { margin-bottom: var(--spacing-2xl); } + +.p-0 { padding: 0; } +.p-xs { padding: var(--spacing-xs); } +.p-sm { padding: var(--spacing-sm); } +.p-md { padding: var(--spacing-md); } +.p-lg { padding: var(--spacing-lg); } +.p-xl { padding: var(--spacing-xl); } +.p-2xl { padding: var(--spacing-2xl); } + +.gap-xs { gap: var(--spacing-xs); } +.gap-sm { gap: var(--spacing-sm); } +.gap-md { gap: var(--spacing-md); } +.gap-lg { gap: var(--spacing-lg); } + +/* Flexbox */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.flex-row { flex-direction: row; } +.items-center { align-items: center; } +.items-start { align-items: flex-start; } +.items-end { align-items: flex-end; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.justify-end { justify-content: flex-end; } +.flex-1 { flex: 1; } +.flex-wrap { flex-wrap: wrap; } + +/* Grid */ +.grid { display: grid; } +.grid-cols-2 { grid-template-columns: repeat(2, 1fr); } +.grid-cols-3 { grid-template-columns: repeat(3, 1fr); } +.grid-cols-4 { grid-template-columns: repeat(4, 1fr); } + +/* Display */ +.hidden { display: none; } +.block { display: block; } +.inline-block { display: inline-block; } + +/* Width */ +.w-full { width: 100%; } +.w-auto { width: auto; } + +/* Background */ +.bg-primary { background-color: var(--color-bg-primary); } +.bg-secondary { background-color: var(--color-bg-secondary); } +.bg-tertiary { background-color: var(--color-bg-tertiary); } + +/* Border */ +.border { border: 1px solid var(--color-gray-200); } +.border-t { border-top: 1px solid var(--color-gray-200); } +.border-b { border-bottom: 1px solid var(--color-gray-200); } +.border-none { border: none; } + +.rounded-sm { border-radius: var(--radius-sm); } +.rounded-md { border-radius: var(--radius-md); } +.rounded-lg { border-radius: var(--radius-lg); } +.rounded-xl { border-radius: var(--radius-xl); } +.rounded-full { border-radius: var(--radius-full); } + +/* Shadow */ +.shadow-sm { box-shadow: var(--shadow-sm); } +.shadow-md { box-shadow: var(--shadow-md); } +.shadow-lg { box-shadow: var(--shadow-lg); } +.shadow-xl { box-shadow: var(--shadow-xl); } +.shadow-none { box-shadow: none; } + +/* Cursor */ +.cursor-pointer { cursor: pointer; } +.cursor-not-allowed { cursor: not-allowed; } + +/* ================================================================= + 11. Responsive Grid System + ================================================================= */ +@media (min-width: 768px) { + .container { + padding: 0 var(--spacing-lg); + } + + .tablet\:grid-cols-2 { grid-template-columns: repeat(2, 1fr); } + .tablet\:grid-cols-3 { grid-template-columns: repeat(3, 1fr); } + .tablet\:grid-cols-4 { grid-template-columns: repeat(4, 1fr); } +} + +@media (min-width: 1024px) { + .container { + padding: 0 var(--spacing-xl); + } + + .desktop\:grid-cols-2 { grid-template-columns: repeat(2, 1fr); } + .desktop\:grid-cols-3 { grid-template-columns: repeat(3, 1fr); } + .desktop\:grid-cols-4 { grid-template-columns: repeat(4, 1fr); } + .desktop\:grid-cols-5 { grid-template-columns: repeat(5, 1fr); } +} diff --git a/design/uiux/style-guide.md b/design/uiux/style-guide.md new file mode 100644 index 0000000..b1f46d5 --- /dev/null +++ b/design/uiux/style-guide.md @@ -0,0 +1,1554 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ + +## ๋ฌธ์„œ ์ •๋ณด + +- ์ž‘์„ฑ์ผ: 2025-10-17 +- ๋ฒ„์ „: 1.0 +- ๊ธฐ๋ฐ˜ ์„ค๊ณ„: UI/UX ์„ค๊ณ„์„œ v1.0 +- ์„ค๊ณ„ ์›์น™: Mobile First, ์ ‘๊ทผ์„ฑ ์šฐ์„ , ์ผ๊ด€์„ฑ + +--- + +## 1. ๋ธŒ๋žœ๋“œ ์•„์ด๋ดํ‹ฐํ‹ฐ + +### 1.1 ๋ธŒ๋žœ๋“œ ๋น„์ „ + +**"AI๋กœ ๊ฐ„ํŽธํ•˜๊ฒŒ, ์„ฑ๊ณต์œผ๋กœ ํ™•์‹คํ•˜๊ฒŒ"** + +์†Œ์ƒ๊ณต์ธ์ด ์ „๋ฌธ ๋งˆ์ผ€ํ„ฐ ์—†์ด๋„ AI์˜ ๋„์›€์œผ๋กœ ํšจ๊ณผ์ ์ธ ์ด๋ฒคํŠธ๋ฅผ ๊ธฐํšํ•˜๊ณ  ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ํ˜์‹ ์ ์ธ ์„œ๋น„์Šค + +### 1.2 ๋””์ž์ธ ์ฒ ํ•™ + +#### ํ˜์‹ ์„ฑ (Innovation) + +- ์ตœ์‹  AI ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•œ ์ž๋™ํ™”๋œ ์ด๋ฒคํŠธ ์ƒ์„ฑ +- ๋ณต์žกํ•œ ๋งˆ์ผ€ํŒ… ํ”„๋กœ์„ธ์Šค๋ฅผ ๋‹จ์ˆœํ™” +- 3๋ถ„ ๋งŒ์— ์™„์„ฑ๋˜๋Š” ์ด๋ฒคํŠธ ์ฝ˜ํ…์ธ  + +#### ์‹ ๋ขฐ์„ฑ (Trust) + +- KT ๋ธŒ๋žœ๋“œ์˜ ์•ˆ์ •์„ฑ๊ณผ ์‹ ๋ขฐ๊ฐ +- ๋ช…ํ™•ํ•œ ํ”„๋กœ์„ธ์Šค์™€ ํˆฌ๋ช…ํ•œ ๊ฒฐ๊ณผ ์ œ๊ณต +- ์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง + +#### ์นœ๊ทผํ•จ (Approachability) + +- ์†Œ์ƒ๊ณต์ธ ๋ˆˆ๋†’์ด์— ๋งž์ถ˜ ์‰ฌ์šด ์ธํ„ฐํŽ˜์ด์Šค +- ์ดˆ๋ณด์ž๋„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ๋ช…ํ™•ํ•œ ์•ˆ๋‚ด +- ๋”ฐ๋œปํ•˜๊ณ  ์นœ๊ทผํ•œ ํ†ค์•ค๋งค๋„ˆ + +### 1.3 ๋ธŒ๋žœ๋“œ ์ปฌ๋Ÿฌ + +**Primary Color: KT Red** + +- ์ •์ฒด์„ฑ, ๋ธŒ๋žœ๋“œ ๋Œ€ํ‘œ ์ƒ‰์ƒ +- ํ–‰๋™ ์œ ๋„(CTA), ๊ฐ•์กฐ ์š”์†Œ +- KT ๋ธŒ๋žœ๋“œ ํ—ค๋ฆฌํ‹ฐ์ง€ ๊ณ„์Šน + +**Secondary Color: AI Blue** + +- AI ๊ธฐ๋Šฅ, ๊ธฐ์ˆ ์  ์‹ ๋ขฐ๊ฐ +- ์ •๋ณด ์ „๋‹ฌ, ์•ˆ๋‚ด ์š”์†Œ +- ํ˜์‹ ๊ณผ ๋ฏธ๋ž˜์ง€ํ–ฅ์„ฑ + +--- + +## 2. ๋””์ž์ธ ์›์น™ + +### 2.1 ๋ช…ํ™•์„ฑ (Clarity) + +**์‚ฌ์šฉ์ž๊ฐ€ ๋ฌด์—‡์„ ํ•ด์•ผ ํ•˜๋Š”์ง€ ํ•ญ์ƒ ๋ช…ํ™•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค** + +- ์ง๊ด€์ ์ธ ์•„์ด์ฝ˜๊ณผ ๋ ˆ์ด๋ธ” ์‚ฌ์šฉ +- ๋ช…ํ™•ํ•œ ์‹œ๊ฐ์  ๊ณ„์ธต ๊ตฌ์กฐ +- ํ˜„์žฌ ์œ„์น˜์™€ ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ํ•ญ์ƒ ํ‘œ์‹œ + +์˜ˆ์‹œ: + +``` +โœ“ ์ข‹์€ ์˜ˆ: "์ƒˆ ์ด๋ฒคํŠธ ๋งŒ๋“ค๊ธฐ" + ํฐ CTA ๋ฒ„ํŠผ +โœ— ๋‚˜์œ ์˜ˆ: "์‹œ์ž‘ํ•˜๊ธฐ" (๋ฌด์—‡์„ ์‹œ์ž‘ํ•˜๋Š”์ง€ ๋ถˆ๋ถ„๋ช…) +``` + +### 2.2 ํšจ์œจ์„ฑ (Efficiency) + +**์ตœ์†Œํ•œ์˜ ๋‹จ๊ณ„๋กœ ๋ชฉํ‘œ๋ฅผ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค** + +- ๋ถˆํ•„์š”ํ•œ ์Šคํ… ์ œ๊ฑฐ +- ์ž๋™ ์™„์„ฑ ๋ฐ ์ถ”์ฒœ ๊ธฐ๋Šฅ ํ™œ์šฉ +- AI๊ฐ€ ๋Œ€๋ถ€๋ถ„์˜ ์ž‘์—… ์ž๋™ํ™” + +๋ชฉํ‘œ: + +- ์ด๋ฒคํŠธ ๊ธฐํš: 10์ดˆ ์ด๋‚ด +- ์ฝ˜ํ…์ธ  ์ƒ์„ฑ: 3๋ถ„ ์ด๋‚ด +- ๋ฐฐํฌ ์„ค์ •: 1๋ถ„ ์ด๋‚ด + +### 2.3 ์‹ ๋ขฐ์„ฑ (Trust) + +**AI ์ฒ˜๋ฆฌ ๊ณผ์ •๊ณผ ๊ฒฐ๊ณผ๋ฅผ ํˆฌ๋ช…ํ•˜๊ฒŒ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค** + +- AI ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ๋ช…์‹œ (์˜ˆ: "AI๊ฐ€ ๋ถ„์„์ค‘์ž…๋‹ˆ๋‹ค ์•ฝ 3์ดˆ ์†Œ์š”") +- ์ค‘๊ฐ„ ๋‹จ๊ณ„ ๊ฒฐ๊ณผ ํ™•์ธ ๊ฐ€๋Šฅ +- ์–ธ์ œ๋“  ์ด์ „ ๋‹จ๊ณ„๋กœ ๋Œ์•„๊ฐˆ ์ˆ˜ ์žˆ์Œ + +### 2.4 ์นœ๊ทผํ•จ (Approachability) + +**์ดˆ๋ณด์ž๋„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค** + +- ์ „๋ฌธ ์šฉ์–ด ์ตœ์†Œํ™” +- ์นœ๊ทผํ•œ ํ†ค์˜ ์•ˆ๋‚ด ๋ฌธ๊ตฌ +- ๋„์›€๋ง๊ณผ ์˜ˆ์‹œ ์ œ๊ณต + +### 2.5 ์ผ๊ด€์„ฑ (Consistency) + +**๋ชจ๋“  ํ™”๋ฉด์—์„œ ๋™์ผํ•œ ํŒจํ„ด์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค** + +- ์ปดํฌ๋„ŒํŠธ ์žฌ์‚ฌ์šฉ +- ์ผ๊ด€๋œ ์ƒ‰์ƒ๊ณผ ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ +- ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ์ธํ„ฐ๋ž™์…˜ + +--- + +## 3. ์ƒ‰์ƒ ์‹œ์Šคํ…œ + +### 3.1 Primary Color (์ฃผ ์ƒ‰์ƒ) + +#### KT Red - ๋ธŒ๋žœ๋“œ ์ •์ฒด์„ฑ + +``` +Main: #E31E24 // CTA ๋ฒ„ํŠผ, ์ฃผ์š” ์•ก์…˜, ํ™œ์„ฑํ™” ์ƒํƒœ +Light: #FF4D52 // ํ˜ธ๋ฒ„ ์ƒํƒœ, ๋ฐฐ๊ฒฝ ๊ฐ•์กฐ +Dark: #C71820 // ๋ˆŒ๋ฆผ ์ƒํƒœ, ์ง„ํ•œ ๊ฐ•์กฐ + +์‚ฌ์šฉ ์˜ˆ์‹œ: +- Primary Button ๋ฐฐ๊ฒฝ +- ํ™œ์„ฑํ™”๋œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์•„์ดํ…œ +- ์ค‘์š”ํ•œ ์•Œ๋ฆผ ๋ฐฐ์ง€ +- ๋งํฌ ํ…์ŠคํŠธ +``` + +#### ์ƒ‰์ƒ ์ ‘๊ทผ์„ฑ + +- White ๋ฐฐ๊ฒฝ ๋Œ€๋น„: 7.2:1 (WCAG AAA) +- Gray-100 ๋ฐฐ๊ฒฝ ๋Œ€๋น„: 6.8:1 (WCAG AAA) + +### 3.2 Secondary Color (๋ณด์กฐ ์ƒ‰์ƒ) + +#### AI Blue - ํ˜์‹ ๊ณผ ์‹ ๋ขฐ + +``` +Main: #0066FF // AI ๊ธฐ๋Šฅ ๊ฐ•์กฐ, ์ •๋ณด ์•„์ด์ฝ˜ +Light: #4D94FF // AI ๋กœ๋”ฉ ๋ฐฐ๊ฒฝ, ์•ˆ๋‚ด ์˜์—ญ +Dark: #004DBF // AI ํ”„๋กœ์„ธ์Šค ์™„๋ฃŒ + +์‚ฌ์šฉ ์˜ˆ์‹œ: +- AI ์ฒ˜๋ฆฌ ์ค‘ ์ƒํƒœ ํ‘œ์‹œ +- ์ •๋ณด ์ œ๊ณต ์˜์—ญ +- ํŠธ๋ Œ๋“œ ๋ถ„์„ ์ฐจํŠธ +- ๋„์›€๋ง ์•„์ด์ฝ˜ +``` + +### 3.3 Grayscale (ํšŒ์ƒ‰์กฐ) + +``` +Black (Gray-900): #1A1A1A // ์ฃผ์š” ์ œ๋ชฉ, ๋ณธ๋ฌธ ํ…์ŠคํŠธ +Gray-700: #4A4A4A // ๋ณด์กฐ ํ…์ŠคํŠธ, ์•„์ด์ฝ˜ +Gray-500: #9E9E9E // ๋น„ํ™œ์„ฑํ™” ํ…์ŠคํŠธ, ํ”Œ๋ ˆ์ด์Šคํ™€๋” +Gray-300: #D9D9D9 // ๊ตฌ๋ถ„์„ , ๋น„ํ™œ์„ฑํ™” ํ…Œ๋‘๋ฆฌ +Gray-100: #F5F5F5 // ๋ฐฐ๊ฒฝ, ๋น„ํ™œ์„ฑํ™” ๋ฒ„ํŠผ +White (Gray-50): #FFFFFF // ์นด๋“œ ๋ฐฐ๊ฒฝ, ๊ธฐ๋ณธ ๋ฐฐ๊ฒฝ + +์ƒ‰์ƒ ๋Œ€๋น„ (White ๋ฐฐ๊ฒฝ ๊ธฐ์ค€): +- Gray-900: 14.2:1 (AAA) +- Gray-700: 8.5:1 (AAA) +- Gray-500: 4.6:1 (AA) +``` + +### 3.4 Semantic Colors (์˜๋ฏธ ์ƒ‰์ƒ) + +``` +Success (์„ฑ๊ณต): #00C853 // ์™„๋ฃŒ, ์Šน์ธ, ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ +Warning (๊ฒฝ๊ณ ): #FFA000 // ์ฃผ์˜, ๋Œ€๊ธฐ ์ค‘, ํ™•์ธ ํ•„์š” +Error (์˜ค๋ฅ˜): #D32F2F // ์˜ค๋ฅ˜, ๊ฑฐ๋ถ€, ์‚ญ์ œ ํ™•์ธ +Info (์ •๋ณด): #0288D1 // ์•ˆ๋‚ด, ํŒ, ์ถ”๊ฐ€ ์ •๋ณด + +์‚ฌ์šฉ ์˜ˆ์‹œ: +- Success: "์ด๋ฒคํŠธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋ฐฐํฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" +- Warning: "AI ์ฒ˜๋ฆฌ๊ฐ€ ํ‰์†Œ๋ณด๋‹ค ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค" +- Error: "์ด๋ฏธ์ง€ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค" +- Info: "ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜์„ธ์š”" +``` + +### 3.5 Gradient (๊ทธ๋ผ๋ฐ์ด์…˜) + +#### AI Feature Gradient + +``` +Primary Gradient: + background: linear-gradient(135deg, #E31E24 0%, #FF4D52 100%); + ์‚ฌ์šฉ: AI ๊ธฐ๋Šฅ ๊ฐ•์กฐ ์นด๋“œ, ํ”„๋ฆฌ๋ฏธ์—„ ๊ธฐ๋Šฅ + +Secondary Gradient: + background: linear-gradient(135deg, #0066FF 0%, #4D94FF 100%); + ์‚ฌ์šฉ: AI ์ฒ˜๋ฆฌ ์ค‘ ๋ฐฐ๊ฒฝ, ์ •๋ณด ๊ฐ•์กฐ ์˜์—ญ +``` + +--- + +## 4. ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ ์‹œ์Šคํ…œ + +### 4.1 Font Family + +**Primary: Pretendard** + +```css +font-family: "Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Helvetica Neue", system-ui, sans-serif; +``` + +**์„ ํƒ ์ด์œ :** + +- ํ•œ๊ธ€ ๊ฐ€๋…์„ฑ์ด ๋›ฐ์–ด๋‚จ +- ๋‹ค์–‘ํ•œ Font Weight ์ง€์› (100~900) +- ๋ชจ๋˜ํ•˜๊ณ  ๊น”๋”ํ•œ ๋””์ž์ธ +- Variable Font ์ง€์›์œผ๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™” + +### 4.2 Type Scale (Mobile First) + +``` +Display (๋ฉ”์ธ ํƒ€์ดํ‹€) +- Size: 28px +- Weight: 700 (Bold) +- Line Height: 1.3 (36px) +- Letter Spacing: -0.5px +- ์‚ฌ์šฉ: ๋ฉ”์ธ ๋Œ€์‹œ๋ณด๋“œ ํƒ€์ดํ‹€, ๋žœ๋”ฉ ํ™”๋ฉด + +H1 (ํ™”๋ฉด ์ œ๋ชฉ) +- Size: 24px +- Weight: 700 (Bold) +- Line Height: 1.3 (31px) +- Letter Spacing: -0.3px +- ์‚ฌ์šฉ: ๊ฐ ํ™”๋ฉด์˜ ๋ฉ”์ธ ์ œ๋ชฉ + +H2 (์„น์…˜ ์ œ๋ชฉ) +- Size: 20px +- Weight: 700 (Bold) +- Line Height: 1.4 (28px) +- Letter Spacing: -0.2px +- ์‚ฌ์šฉ: ์นด๋“œ ๊ทธ๋ฃน, ์„น์…˜ ๊ตฌ๋ถ„ + +H3 (์นด๋“œ ์ œ๋ชฉ) +- Size: 18px +- Weight: 600 (SemiBold) +- Line Height: 1.4 (25px) +- Letter Spacing: 0px +- ์‚ฌ์šฉ: ์นด๋“œ ์ œ๋ชฉ, ๋ชจ๋‹ฌ ์ œ๋ชฉ + +Body-Large (ํฐ ๋ณธ๋ฌธ) +- Size: 16px +- Weight: 400 (Regular) +- Line Height: 1.5 (24px) +- Letter Spacing: 0px +- ์‚ฌ์šฉ: ์ž…๋ ฅ ํ•„๋“œ, ์ค‘์š”ํ•œ ๋ณธ๋ฌธ + +Body-Medium (๊ธฐ๋ณธ ๋ณธ๋ฌธ) +- Size: 14px +- Weight: 400 (Regular) +- Line Height: 1.5 (21px) +- Letter Spacing: 0px +- ์‚ฌ์šฉ: ์ผ๋ฐ˜ ๋ณธ๋ฌธ, ์„ค๋ช… ํ…์ŠคํŠธ + +Body-Small (์ž‘์€ ๋ณธ๋ฌธ) +- Size: 12px +- Weight: 400 (Regular) +- Line Height: 1.5 (18px) +- Letter Spacing: 0px +- ์‚ฌ์šฉ: ์บก์…˜, ๋ณด์กฐ ์ •๋ณด, ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ + +Button (๋ฒ„ํŠผ ๋ ˆ์ด๋ธ”) +- Size: 16px +- Weight: 600 (SemiBold) +- Line Height: 1.5 (24px) +- Letter Spacing: 0px +- ์‚ฌ์šฉ: ๋ชจ๋“  ๋ฒ„ํŠผ ํ…์ŠคํŠธ +``` + +### 4.3 Font Weights + +``` +Regular (400): ์ผ๋ฐ˜ ๋ณธ๋ฌธ, ์„ค๋ช… +Medium (500): ๊ฐ•์กฐํ•˜๊ณ  ์‹ถ์€ ๋ณธ๋ฌธ +SemiBold (600): ๋ฒ„ํŠผ, ์ค‘์š” ์ •๋ณด +Bold (700): ์ œ๋ชฉ, ํ—ค๋”ฉ +``` + +### 4.4 Responsive Typography + +**Tablet (768px~)** + +``` +Display: 32px (+4px) +H1: 28px (+4px) +H2: 22px (+2px) +H3: 20px (+2px) +Body-L: 18px (+2px) +Body-M: 16px (+2px) +Body-S: 14px (+2px) +Button: 16px (์œ ์ง€) +``` + +**Desktop (1024px~)** + +``` +Display: 36px (+8px) +H1: 32px (+8px) +H2: 24px (+4px) +H3: 20px (+2px) +Body-L: 18px (+2px) +Body-M: 16px (+2px) +Body-S: 14px (+2px) +Button: 16px (์œ ์ง€) +``` + +--- + +## 5. ๊ฐ„๊ฒฉ ์‹œ์Šคํ…œ (Spacing) + +### 5.1 Base Unit + +**4px Grid System** - ๋ชจ๋“  ๊ฐ„๊ฒฉ์€ 4์˜ ๋ฐฐ์ˆ˜ + +### 5.2 Spacing Scale + +``` +XS (Extra Small): 4px // ๋งค์šฐ ์ž‘์€ ์š”์†Œ ๊ฐ„ (์•„์ด์ฝ˜-ํ…์ŠคํŠธ) +S (Small): 8px // ๊ด€๋ จ ์š”์†Œ ๊ฐ„ (๋ ˆ์ด๋ธ”-์ž…๋ ฅ, ๋ฒ„ํŠผ ๋‚ด๋ถ€) +M (Medium): 16px // ์„น์…˜ ๋‚ด ์š”์†Œ ๊ฐ„ (์นด๋“œ ๋‚ด ํ•ญ๋ชฉ) +L (Large): 24px // ์นด๋“œ ๋‚ด๋ถ€ ํŒจ๋”ฉ, ์„น์…˜ ์ œ๋ชฉ ํ•˜๋‹จ +XL (Extra Large): 32px // ์„น์…˜ ๊ฐ„ ๊ฐ„๊ฒฉ +2XL (2X Large): 48px // ํ™”๋ฉด ์ƒํ•˜๋‹จ ์—ฌ๋ฐฑ +``` + +### 5.3 Component Spacing + +#### Button + +``` +Padding (์„ธ๋กœ x ๊ฐ€๋กœ): +- Large: 16px x 24px (๋†’์ด 48px) +- Medium: 12px x 20px (๋†’์ด 44px) +- Small: 8px x 16px (๋†’์ด 36px) + +Button ๊ฐ„ ๊ฐ„๊ฒฉ: 12px (S + XS) +``` + +#### Card + +``` +๋‚ด๋ถ€ ํŒจ๋”ฉ: 24px (L) +์นด๋“œ ๊ฐ„ ๊ฐ„๊ฒฉ: 16px (M) +``` + +#### Input Field + +``` +๋‚ด๋ถ€ ํŒจ๋”ฉ: 16px (M) +๋ ˆ์ด๋ธ”-์ž…๋ ฅ ๊ฐ„๊ฒฉ: 8px (S) +์ž…๋ ฅ ํ•„๋“œ ๊ฐ„ ๊ฐ„๊ฒฉ: 16px (M) +``` + +#### Screen Margins + +``` +Mobile: 20px (์–‘์ชฝ) +Tablet: 40px (์–‘์ชฝ) +Desktop: 80px (์–‘์ชฝ, ์ตœ๋Œ€ 1200px container) +``` + +### 5.4 Touch Target + +**WCAG 2.1 AA ์ค€์ˆ˜** + +``` +์ตœ์†Œ ํ„ฐ์น˜ ์˜์—ญ: 44 x 44px +๊ถŒ์žฅ ํ„ฐ์น˜ ์˜์—ญ: 48 x 48px + +์ ์šฉ ๋Œ€์ƒ: +- ๋ชจ๋“  ๋ฒ„ํŠผ +- ํƒญ ํ•ญ๋ชฉ +- ์ฒดํฌ๋ฐ•์Šค, ๋ผ๋””์˜ค ๋ฒ„ํŠผ +- ๋งํฌ๊ฐ€ ์žˆ๋Š” ์นด๋“œ +``` + +--- + +## 6. ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ + +### 6.1 Button (๋ฒ„ํŠผ) + +#### Primary Button + +``` +๋ฐฐ๊ฒฝ: #E31E24 (KT Red) +ํ…์ŠคํŠธ: #FFFFFF (White) +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 8px +๊ทธ๋ฆผ์ž: 0 2px 4px rgba(227, 30, 36, 0.2) + +์ƒํƒœ๋ณ„: +- Default: ๋ฐฐ๊ฒฝ #E31E24 +- Hover: ๋ฐฐ๊ฒฝ #FF4D52 (10% ๋ฐ๊ฒŒ) +- Pressed: ๋ฐฐ๊ฒฝ #C71820 (10% ์–ด๋‘ก๊ฒŒ) +- Disabled: ๋ฐฐ๊ฒฝ #D9D9D9, ํ…์ŠคํŠธ #9E9E9E +``` + +#### Secondary Button + +``` +๋ฐฐ๊ฒฝ: #FFFFFF (White) +ํ…์ŠคํŠธ: #E31E24 (KT Red) +ํ…Œ๋‘๋ฆฌ: 2px solid #E31E24 +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 8px + +์ƒํƒœ๋ณ„: +- Default: ํ…Œ๋‘๋ฆฌ #E31E24 +- Hover: ๋ฐฐ๊ฒฝ #FFF5F5 (5% Red tint) +- Pressed: ๋ฐฐ๊ฒฝ #FFEBEB (10% Red tint) +- Disabled: ํ…Œ๋‘๋ฆฌ #D9D9D9, ํ…์ŠคํŠธ #9E9E9E +``` + +#### Text Button + +``` +๋ฐฐ๊ฒฝ: ์—†์Œ +ํ…์ŠคํŠธ: #E31E24 (KT Red) +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 8px + +์ƒํƒœ๋ณ„: +- Default: ํ…์ŠคํŠธ #E31E24 +- Hover: ๋ฐฐ๊ฒฝ #FFF5F5 +- Pressed: ๋ฐฐ๊ฒฝ #FFEBEB +- Disabled: ํ…์ŠคํŠธ #9E9E9E +``` + +#### Button Sizes + +``` +Large: +- ๋†’์ด: 48px +- ํŒจ๋”ฉ: 16px x 24px +- ํฐํŠธ: Button (16px SemiBold) +- ์‚ฌ์šฉ: ์ฃผ์š” CTA + +Medium: +- ๋†’์ด: 44px +- ํŒจ๋”ฉ: 12px x 20px +- ํฐํŠธ: Body-M (14px SemiBold) +- ์‚ฌ์šฉ: ์ผ๋ฐ˜ ์•ก์…˜ + +Small: +- ๋†’์ด: 36px +- ํŒจ๋”ฉ: 8px x 16px +- ํฐํŠธ: Body-S (12px SemiBold) +- ์‚ฌ์šฉ: ๋ณด์กฐ ์•ก์…˜ +``` + +### 6.2 Card (์นด๋“œ) + +#### Default Card + +``` +๋ฐฐ๊ฒฝ: #FFFFFF (White) +ํ…Œ๋‘๋ฆฌ: 1px solid #E0E0E0 +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 12px +๊ทธ๋ฆผ์ž: 0 2px 8px rgba(0, 0, 0, 0.08) +๋‚ด๋ถ€ ํŒจ๋”ฉ: 24px + +์ƒํƒœ๋ณ„: +- Default: ํ…Œ๋‘๋ฆฌ #E0E0E0 +- Hover: ํ…Œ๋‘๋ฆฌ #E31E24, ๊ทธ๋ฆผ์ž 0 4px 12px rgba(227, 30, 36, 0.12) +- Selected: ํ…Œ๋‘๋ฆฌ 2px solid #E31E24 +``` + +#### Event Card (์ด๋ฒคํŠธ ์นด๋“œ) + +``` +๋ฐฐ๊ฒฝ: #FFFFFF +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 12px +๊ทธ๋ฆผ์ž: 0 2px 8px rgba(0, 0, 0, 0.08) + +๊ตฌ์กฐ: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [์ด๋ฏธ์ง€ ์ธ๋„ค์ผ 16:9] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ H3 ์ œ๋ชฉ โ”‚ +โ”‚ Body-S ๋ฉ”ํƒ€ ์ •๋ณด โ”‚ +โ”‚ [์ƒํƒœ ๋ฐฐ์ง€] [CTA ๋ฒ„ํŠผ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### Selection Card (์„ ํƒํ˜• ์นด๋“œ) + +``` +์‚ฌ์šฉ: ์˜ต์…˜ ์„ ํƒ ํ™”๋ฉด (09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ.html) +ํŠน์ง•: ์นด๋“œ ์ „์ฒด๊ฐ€ ์„ ํƒ ๊ฐ€๋Šฅํ•œ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์˜์—ญ + +๋ฐฐ๊ฒฝ: #FFFFFF +ํ…Œ๋‘๋ฆฌ: 3px solid transparent +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 12px +๊ทธ๋ฆผ์ž: 0 2px 8px rgba(0, 0, 0, 0.08) +๋‚ด๋ถ€ ํŒจ๋”ฉ: 24px +์ปค์„œ: pointer + +์ƒํƒœ๋ณ„: +- Default: + - ํ…Œ๋‘๋ฆฌ transparent + - ๊ทธ๋ฆผ์ž 0 2px 8px rgba(0, 0, 0, 0.08) + +- Hover: + - transform: translateY(-2px) + - ๊ทธ๋ฆผ์ž 0 8px 16px rgba(0, 0, 0, 0.1) + +- Selected: + - ํ…Œ๋‘๋ฆฌ 3px solid #E31E24 + - ๊ทธ๋ฆผ์ž 0 4px 12px rgba(227, 30, 36, 0.2) + - ์šฐ์ธก ์ƒ๋‹จ ์ฒดํฌ ๋ฐฐ์ง€ ํ‘œ์‹œ + +๊ตฌ์กฐ: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [โœ“] โ”‚ โ† ์„ ํƒ ๋ฐฐ์ง€ (์กฐ๊ฑด๋ถ€) +โ”‚ [๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ด๋ฏธ์ง€ 1:1] โ”‚ +โ”‚ โ”‚ +โ”‚ H3 ์˜ต์…˜ ์ œ๋ชฉ โ”‚ +โ”‚ Body-S ์„ค๋ช… โ”‚ +โ”‚ โ”‚ +โ”‚ [ํฌ๊ฒŒ๋ณด๊ธฐ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +Selected Badge (์ฒดํฌ ๋ฐฐ์ง€): +- ์œ„์น˜: ์šฐ์ธก ์ƒ๋‹จ (absolute) +- ํฌ๊ธฐ: 32 x 32px +- ๋ฐฐ๊ฒฝ: #E31E24 +- ์•„์ด์ฝ˜: check (White, 20px) +- ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 50% (์›ํ˜•) +- Display: none (๊ธฐ๋ณธ), flex (์„ ํƒ ์‹œ) +- z-index: 10 + +Image Preview: +- ๋น„์œจ: 1:1 (aspect-ratio) +- ๋ฐฐ๊ฒฝ: #F5F5F5 (ํ”Œ๋ ˆ์ด์Šคํ™€๋”) +- ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 12px +- object-fit: cover + +Radio Button: +- Display: none (์ˆจ๊น€) +- ๊ธฐ๋Šฅ: ์œ ์ง€ (ํผ ์ œ์ถœ์šฉ) +- ์ ‘๊ทผ์„ฑ: ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ ์ง€์› + +์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜: +- Duration: 0.3s +- Easing: ease + +์ฃผ์˜์‚ฌํ•ญ: +- ์นด๋“œ ๋‚ด๋ถ€ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง ๋ฐฉ์ง€ ํ•„์š” + - event.stopPropagation() ์‚ฌ์šฉ +- ์นด๋“œ ํด๋ฆญ๊ณผ ๋ณด์กฐ ์•ก์…˜ ๋ฒ„ํŠผ ํด๋ฆญ ๊ตฌ๋ถ„ +- ํ‚ค๋ณด๋“œ ์ ‘๊ทผ์„ฑ: Enter/Space๋กœ ์„ ํƒ ๊ฐ€๋Šฅ +``` + +#### Stat Card (์ง€ํ‘œ ์นด๋“œ) + +``` +๋ฐฐ๊ฒฝ: Gradient ๋˜๋Š” Solid +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 16px +๋‚ด๋ถ€ ํŒจ๋”ฉ: 24px + +๊ตฌ์กฐ: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [์•„์ด์ฝ˜] Body-S ๋ ˆ์ด๋ธ” โ”‚ +โ”‚ Display ์ˆ˜์น˜ โ”‚ +โ”‚ Body-S ๋ณ€ํ™”์œจ +32% โ†‘ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 6.3 Input Field (์ž…๋ ฅ ํ•„๋“œ) + +#### Text Input + +``` +๋ฐฐ๊ฒฝ: #FFFFFF (White) +ํ…Œ๋‘๋ฆฌ: 1px solid #D9D9D9 +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 8px +๋†’์ด: 48px +๋‚ด๋ถ€ ํŒจ๋”ฉ: 16px +ํฐํŠธ: Body-L (16px Regular) + +์ƒํƒœ๋ณ„: +- Default: ํ…Œ๋‘๋ฆฌ #D9D9D9 +- Focus: ํ…Œ๋‘๋ฆฌ 2px solid #0066FF, ๊ทธ๋ฆผ์ž 0 0 0 4px rgba(0, 102, 255, 0.1) +- Error: ํ…Œ๋‘๋ฆฌ 2px solid #D32F2F, ๊ทธ๋ฆผ์ž 0 0 0 4px rgba(211, 47, 47, 0.1) +- Disabled: ๋ฐฐ๊ฒฝ #F5F5F5, ํ…Œ๋‘๋ฆฌ #E0E0E0, ํ…์ŠคํŠธ #9E9E9E +- Filled: ๋ฐฐ๊ฒฝ ์œ ์ง€, ํ…์ŠคํŠธ #1A1A1A +``` + +#### Textarea + +``` +๋ฐฐ๊ฒฝ: #FFFFFF +ํ…Œ๋‘๋ฆฌ: 1px solid #D9D9D9 +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 8px +์ตœ์†Œ ๋†’์ด: 120px (์•ฝ 5์ค„) +๋‚ด๋ถ€ ํŒจ๋”ฉ: 16px +ํฐํŠธ: Body-M (14px Regular) +Line Height: 1.5 + +Resize: vertical (์„ธ๋กœ ๋ฐฉํ–ฅ๋งŒ) +``` + +#### Editable Field (์ธ๋ผ์ธ ํŽธ์ง‘) + +``` +์‚ฌ์šฉ: 08-AI์ด๋ฒคํŠธ์ถ”์ฒœ.html (์ œ๋ชฉ, ๊ฒฝํ’ˆ ํŽธ์ง‘) +ํŠน์ง•: contenteditable์„ ํ™œ์šฉํ•œ ์ธ๋ผ์ธ ํŽธ์ง‘ + +๋ฐฐ๊ฒฝ: ํˆฌ๋ช… (๊ธฐ๋ณธ), #F5F5F5 (hover), #FFFFFF (focus) +ํ…Œ๋‘๋ฆฌ: 1px dashed #D9D9D9 +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 4px +๋‚ด๋ถ€ ํŒจ๋”ฉ: 4px 8px +ํฐํŠธ: ๋ฌธ๋งฅ์— ๋”ฐ๋ฆ„ (์ œ๋ชฉ: H3, ๊ฒฝํ’ˆ: Body) +์ปค์„œ: text + +์ƒํƒœ๋ณ„: +- Default: + - ํ…Œ๋‘๋ฆฌ: 1px dashed #D9D9D9 + - ๋ฐฐ๊ฒฝ: ํˆฌ๋ช… + +- Hover: + - ํ…Œ๋‘๋ฆฌ: 1px dashed #E31E24 + - ๋ฐฐ๊ฒฝ: #F5F5F5 + +- Focus: + - outline: none + - ํ…Œ๋‘๋ฆฌ: 1px solid #E31E24 + - ๋ฐฐ๊ฒฝ: #FFFFFF + +์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜: +- Duration: 200ms (Fast) +- Easing: ease-out + +์ ‘๊ทผ์„ฑ: +- contenteditable="true" +- role="textbox" +- aria-label="ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ํ•„๋“œ๋ช…" + +์ฃผ์˜์‚ฌํ•ญ: +- ๋นˆ ๊ฐ’ ๋ฐฉ์ง€ (์ตœ์†Œ 1์ž ์ด์ƒ) +- Enter ํ‚ค๋กœ ํŽธ์ง‘ ์™„๋ฃŒ (blur) +- maxLength ์†์„ฑ์œผ๋กœ ๊ธธ์ด ์ œํ•œ +``` + +#### Placeholder + +``` +์ƒ‰์ƒ: #9E9E9E (Gray-500) +ํฐํŠธ: Body-L (16px Regular) +์Šคํƒ€์ผ: italic (์„ ํƒ์ ) +``` + +### 6.4 Checkbox & Radio + +#### Checkbox + +``` +ํฌ๊ธฐ: 24 x 24px (ํ„ฐ์น˜ ์˜์—ญ 44px) +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 4px +ํ…Œ๋‘๋ฆฌ: 2px solid #D9D9D9 + +์ƒํƒœ๋ณ„: +- Unchecked: ๋ฐฐ๊ฒฝ White, ํ…Œ๋‘๋ฆฌ #D9D9D9 +- Checked: ๋ฐฐ๊ฒฝ #E31E24, ์ฒดํฌ๋งˆํฌ White +- Disabled: ๋ฐฐ๊ฒฝ #F5F5F5, ํ…Œ๋‘๋ฆฌ #E0E0E0 +``` + +#### Radio Button + +``` +ํฌ๊ธฐ: 24 x 24px (ํ„ฐ์น˜ ์˜์—ญ 44px) +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 50% (์›ํ˜•) +ํ…Œ๋‘๋ฆฌ: 2px solid #D9D9D9 + +์ƒํƒœ๋ณ„: +- Unselected: ๋ฐฐ๊ฒฝ White, ํ…Œ๋‘๋ฆฌ #D9D9D9 +- Selected: ํ…Œ๋‘๋ฆฌ #E31E24, ๋‚ด๋ถ€ ์  12px #E31E24 +- Disabled: ๋ฐฐ๊ฒฝ #F5F5F5, ํ…Œ๋‘๋ฆฌ #E0E0E0 +``` + +### 6.5 Budget Navigation (์˜ˆ์‚ฐ ์„ ํƒ ํƒญ) + +``` +์‚ฌ์šฉ: 08-AI์ด๋ฒคํŠธ์ถ”์ฒœ.html +ํŠน์ง•: Sticky ๋„ค๋น„๊ฒŒ์ด์…˜์œผ๋กœ ์˜ˆ์‚ฐ ์„น์…˜ ์ด๋™ + +๋ฐฐ๊ฒฝ: #F5F5F5 (๋ฐฐ๊ฒฝ์ƒ‰) +์œ„์น˜: sticky, top 56px (Header ์•„๋ž˜) +z-index: 10 +ํŒจ๋”ฉ: 16px 0 + +๊ตฌ์กฐ: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๐Ÿ’ฐ ์ €๋น„์šฉ] [๐Ÿ’ฐ๐Ÿ’ฐ ์ค‘๋น„์šฉ] [๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ ๊ณ ๋น„์šฉ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +๋ฒ„ํŠผ: +- ํฌ๊ธฐ: Medium (44px ๋†’์ด) +- ๊ฐ„๊ฒฉ: 8px +- flex-1 (๊ท ๋“ฑ ๋ถ„๋ฐฐ) + +์ƒํƒœ๋ณ„: +- Default: + - ๋ฐฐ๊ฒฝ: White + - ํ…์ŠคํŠธ: #4A4A4A + - ํ…Œ๋‘๋ฆฌ: 1px solid #E0E0E0 + +- Active: + - ๋ฐฐ๊ฒฝ: #E31E24 + - ํ…์ŠคํŠธ: White + - ํ…Œ๋‘๋ฆฌ: none + - ๊ทธ๋ฆผ์ž: 0 2px 4px rgba(227, 30, 36, 0.2) + +- Hover (๋น„ํ™œ์„ฑํ™” ํƒญ): + - ๋ฐฐ๊ฒฝ: #FFF5F5 + - ํ…Œ๋‘๋ฆฌ: 1px solid #E31E24 + +์ƒํ˜ธ์ž‘์šฉ: +- ํด๋ฆญ ์‹œ: ํ•ด๋‹น ์˜ˆ์‚ฐ ์„น์…˜์œผ๋กœ smooth scroll +- Scroll ์‹œ: ํ˜„์žฌ ๋ณด์ด๋Š” ์„น์…˜์— ๋งž์ถฐ Active ์ƒํƒœ ๋ณ€๊ฒฝ +``` + +### 6.6 Option Card (์ด๋ฒคํŠธ ์˜ต์…˜ ์นด๋“œ) + +``` +์‚ฌ์šฉ: 08-AI์ด๋ฒคํŠธ์ถ”์ฒœ.html +ํŠน์ง•: ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ ๋ฐฐ์ง€ + Editable Field + Radio ์„ ํƒ + +๋ฐฐ๊ฒฝ: #FFFFFF +ํ…Œ๋‘๋ฆฌ: 1px solid #E0E0E0 +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 12px +๊ทธ๋ฆผ์ž: 0 2px 8px rgba(0, 0, 0, 0.08) +๋‚ด๋ถ€ ํŒจ๋”ฉ: 24px + +๊ตฌ์กฐ: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [์˜จ๋ผ์ธ] ๋ฐฐ์ง€ [โ—ฏ] โ”‚ +โ”‚ โ”‚ +โ”‚ ์ œ๋ชฉ (editable) โ”‚ +โ”‚ ๊ฒฝํ’ˆ๋ช… (editable) โ”‚ +โ”‚ โ”‚ +โ”‚ ์ฐธ์—ฌ ๋ฐฉ๋ฒ•: ... โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“Š ์˜ˆ์ƒ ์ฐธ์—ฌ์ž: 200๋ช… โ”‚ +โ”‚ ๐Ÿ’ฐ ์˜ˆ์ƒ ๋น„์šฉ: 30๋งŒ์› โ”‚ +โ”‚ ๐Ÿ“ˆ ์˜ˆ์ƒ ROI: 180% โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ ๋ฐฐ์ง€: +- ํฌ๊ธฐ: ํŒจ๋”ฉ 4px 12px +- ํฐํŠธ: Body-S (12px SemiBold) +- ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 12px (pill) +- ์˜จ๋ผ์ธ: ๋ฐฐ๊ฒฝ #E3F2FD (Blue), ํ…์ŠคํŠธ #0066FF +- ์˜คํ”„๋ผ์ธ: ๋ฐฐ๊ฒฝ #FCE4EC (Pink), ํ…์ŠคํŠธ #E31E24 + +Radio ๋ฒ„ํŠผ: +- ์œ„์น˜: ์šฐ์ธก ์ƒ๋‹จ +- ํฌ๊ธฐ: 24 x 24px +- Display: visible (๊ธฐ๋ณธ ๋ณด์ž„) + +Editable Field: +- ์Šคํƒ€์ผ: ์ธ๋ผ์ธ ํŽธ์ง‘ (์ƒ์„ธ ๋‚ด์šฉ์€ 6.3 ์ฐธ์กฐ) +- Hover: ์ ์„  ํ…Œ๋‘๋ฆฌ๋กœ ํŽธ์ง‘ ๊ฐ€๋Šฅ ํ‘œ์‹œ +- Focus: ์‹ค์„  ํ…Œ๋‘๋ฆฌ๋กœ ๋ณ€๊ฒฝ + +ํ†ต๊ณ„ ์ •๋ณด: +- ํฐํŠธ: Body-S (12px Regular) +- ์ƒ‰์ƒ: #4A4A4A (Secondary) +- ์•„์ด์ฝ˜: Material Icons (16px) +``` + +### 6.7 Bottom Navigation + +``` +๋ฐฐ๊ฒฝ: #FFFFFF (White) +๋†’์ด: 60px +๊ทธ๋ฆผ์ž: 0 -2px 8px rgba(0, 0, 0, 0.08) +์•„์ดํ…œ ๊ฐœ์ˆ˜: 4๊ฐœ (ํ™ˆ, ์ด๋ฒคํŠธ, ๋ถ„์„, ๋งˆ์ด) + +์•„์ดํ…œ ๊ตฌ์กฐ: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [์•„์ด์ฝ˜] โ”‚ +โ”‚ ๋ ˆ์ด๋ธ” โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +์•„์ด์ฝ˜: +- ํฌ๊ธฐ: 24 x 24px +- ์Šคํƒ€์ผ: Outlined (๊ธฐ๋ณธ), Filled (ํ™œ์„ฑํ™”) + +๋ ˆ์ด๋ธ”: +- ํฐํŠธ: Body-S (12px Regular) + +์ƒ‰์ƒ: +- ๋น„ํ™œ์„ฑํ™”: ์•„์ด์ฝ˜/ํ…์ŠคํŠธ #9E9E9E +- ํ™œ์„ฑํ™”: ์•„์ด์ฝ˜/ํ…์ŠคํŠธ #E31E24 +- ๋ฐฐ๊ฒฝ: ํ™œ์„ฑํ™” ์‹œ ํˆฌ๋ช… + +๊ฐ„๊ฒฉ: +- ์•„์ด์ฝ˜-๋ ˆ์ด๋ธ”: 4px (XS) +``` + +### 6.8 Stepper (๋‹จ๊ณ„ ํ‘œ์‹œ) + +#### Progress Stepper (AI ์ด๋ฒคํŠธ ์ƒ์„ฑ) + +``` +์ „์ฒด ๋†’์ด: 48px +๋ฐฐ๊ฒฝ: #F5F5F5 +์ง„ํ–‰๋ฅ  ๋ฐ”: #E31E24 +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 24px + +๊ตฌ์กฐ: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 3/7 ๋‹จ๊ณ„ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +ํ…์ŠคํŠธ: +- ํฐํŠธ: Body-M (14px SemiBold) +- ์ƒ‰์ƒ: #1A1A1A +``` + +#### Step Indicator (๋‹จ๊ณ„๋ณ„ ํ‘œ์‹œ) + +``` +โ”Œโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ” +โ”‚ โœ“ โ”‚โ”€ โ”‚ 2 โ”‚โ”€ โ”‚ 3 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”˜ +์™„๋ฃŒ ์ง„ํ–‰์ค‘ ๋Œ€๊ธฐ + +์› ํฌ๊ธฐ: 32px +์„  ๋‘๊ป˜: 2px +๊ฐ„๊ฒฉ: 8px (์„  ๊ธธ์ด ๊ฐ€๋ณ€) + +์ƒ‰์ƒ: +- ์™„๋ฃŒ: ๋ฐฐ๊ฒฝ #00C853, ์•„์ด์ฝ˜ White +- ์ง„ํ–‰์ค‘: ๋ฐฐ๊ฒฝ #E31E24, ์ˆซ์ž White +- ๋Œ€๊ธฐ: ๋ฐฐ๊ฒฝ #F5F5F5, ํ…Œ๋‘๋ฆฌ #D9D9D9, ์ˆซ์ž #9E9E9E +``` + +--- + +## 7. ๋ฐ˜์‘ํ˜• ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ + +### 7.1 Breakpoints + +``` +Mobile (๊ธฐ๋ณธ): +- Range: 320px ~ 767px +- Container: 100% - 40px (์–‘์ชฝ 20px ๋งˆ์ง„) +- Columns: 4 columns +- Gutter: 16px +- ๋ ˆ์ด์•„์›ƒ: 1์—ด ์Šคํƒ + +Tablet: +- Range: 768px ~ 1023px +- Container: 100% - 80px (์–‘์ชฝ 40px ๋งˆ์ง„) +- Columns: 8 columns +- Gutter: 24px +- ๋ ˆ์ด์•„์›ƒ: 2์—ด ๊ทธ๋ฆฌ๋“œ + +Desktop: +- Range: 1024px ์ด์ƒ +- Container: Max 1200px (์ค‘์•™ ์ •๋ ฌ) +- Columns: 12 columns +- Gutter: 32px +- ๋ ˆ์ด์•„์›ƒ: 3์—ด ๊ทธ๋ฆฌ๋“œ + ์‚ฌ์ด๋“œ๋ฐ” +``` + +### 7.2 CSS Media Queries + +```css +/* Mobile First - ๊ธฐ๋ณธ ์Šคํƒ€์ผ */ +.component { + /* 320px ~ 767px */ +} + +/* Tablet */ +@media (min-width: 768px) { + .component { + /* Tablet ์Šคํƒ€์ผ */ + } +} + +/* Desktop */ +@media (min-width: 1024px) { + .component { + /* Desktop ์Šคํƒ€์ผ */ + } +} +``` + +### 7.3 Grid System + +#### Mobile Grid (4 Columns) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1 โ”‚ 2 โ”‚ 3 โ”‚ 4 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”˜ + +์‚ฌ์šฉ ์˜ˆ: +- Full Width: span 4 +- Half Width: span 2 +``` + +#### Tablet Grid (8 Columns) + +``` +โ”Œโ”€โ”€โ”ฌโ”€โ”€โ”ฌโ”€โ”€โ”ฌโ”€โ”€โ”ฌโ”€โ”€โ”ฌโ”€โ”€โ”ฌโ”€โ”€โ”ฌโ”€โ”€โ” +โ”‚ 1โ”‚ 2โ”‚ 3โ”‚ 4โ”‚ 5โ”‚ 6โ”‚ 7โ”‚ 8โ”‚ +โ””โ”€โ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”˜ + +์‚ฌ์šฉ ์˜ˆ: +- Full Width: span 8 +- Half Width: span 4 (2๊ฐœ ์นด๋“œ) +- 1/3 Width: span 2-3 (3๊ฐœ ์นด๋“œ) +``` + +#### Desktop Grid (12 Columns) + +``` +โ”Œโ”ฌโ”ฌโ”ฌโ”ฌโ”ฌโ”ฌโ”ฌโ”ฌโ”ฌโ”ฌโ”ฌโ”ฌโ” +โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚โ”‚ +โ””โ”ดโ”ดโ”ดโ”ดโ”ดโ”ดโ”ดโ”ดโ”ดโ”ดโ”ดโ”ดโ”˜ + +์‚ฌ์šฉ ์˜ˆ: +- Main Content: span 8-9 (์™ผ์ชฝ) +- Sidebar: span 3-4 (์˜ค๋ฅธ์ชฝ) +- 3 Column Cards: span 4 each +``` + +--- + +## 8. ์„œ๋น„์Šค ํŠนํ™” ์ปดํฌ๋„ŒํŠธ + +### 8.1 AI ์ฒ˜๋ฆฌ ์ƒํƒœ ์ปดํฌ๋„ŒํŠธ + +#### Loading Skeleton + +``` +๋ฐฐ๊ฒฝ: #F5F5F5 +์• ๋‹ˆ๋ฉ”์ด์…˜: Shimmer (์ขŒโ†’์šฐ ๋ฐ˜์ง์ž„) +Duration: 1.5s infinite + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ–“โ–“โ–“โ–“โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ โ”‚ โ† ์ œ๋ชฉ +โ”‚ โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ โ”‚ โ† ๋ณธ๋ฌธ +โ”‚ โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ โ”‚ +โ”‚ โ–“โ–“โ–“โ–‘โ–‘โ–‘ โ–“โ–“โ–“โ–‘โ–‘โ–‘ โ–“โ–“โ–“โ–‘โ–‘ โ”‚ โ† ๋ฒ„ํŠผ๋“ค +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +์‚ฌ์šฉ: AI ์ด๋ฏธ์ง€ ์ƒ์„ฑ, ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋“ฑ +``` + +#### Progress Indicator (AI ๋‹จ๊ณ„๋ณ„) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๐Ÿค– AI๊ฐ€ ํŠธ๋ Œ๋“œ๋ฅผ ๋ถ„์„์ค‘์ž…๋‹ˆ๋‹ค โ”‚ +โ”‚ โ”‚ +โ”‚ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 35% โ”‚ +โ”‚ โ”‚ +โ”‚ ์˜ˆ์ƒ ์‹œ๊ฐ„: ์•ฝ 2์ดˆ ๋‚จ์Œ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +๋ฐฐ๊ฒฝ: White +ํ…Œ๋‘๋ฆฌ: 1px solid #E0E0E0 +์ง„ํ–‰๋ฐ”: #0066FF (AI Blue) +ํ…์ŠคํŠธ: Body-M #4A4A4A +``` + +#### Spinner (๊ฐ„๋‹จํ•œ ๋กœ๋”ฉ) + +``` +ํฌ๊ธฐ: 32px +๋‘๊ป˜: 3px +์ƒ‰์ƒ: #E31E24 (Primary) ๋˜๋Š” #0066FF (AI Blue) +์• ๋‹ˆ๋ฉ”์ด์…˜: rotate 360deg, 0.8s linear infinite + +์‚ฌ์šฉ: ๋ฒ„ํŠผ ๋‚ด๋ถ€, ์ž‘์€ ์•ก์…˜ ๋กœ๋”ฉ +``` + +### 8.2 AI ๊ฒฐ๊ณผ ์นด๋“œ + +#### AI ์ƒ์„ฑ ์˜ต์…˜ ์นด๋“œ (์„ ํƒํ˜•) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ—ฏ [์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ] โ”‚ +โ”‚ โ”‚ +โ”‚ H3 ์˜ต์…˜ ์ œ๋ชฉ โ”‚ +โ”‚ Body-S ์„ค๋ช… โ”‚ +โ”‚ [์žฌ์ƒ์„ฑ ๋ฒ„ํŠผ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +์ƒํƒœ๋ณ„: +- ๋น„์„ ํƒ: ํ…Œ๋‘๋ฆฌ 1px #E0E0E0 +- ์„ ํƒ: ํ…Œ๋‘๋ฆฌ 2px #E31E24, ๋ฐฐ๊ฒฝ #FFF5F5 +- ํ˜ธ๋ฒ„: ๊ทธ๋ฆผ์ž 0 4px 12px rgba(0,0,0,0.1) + +๋ผ๋””์˜ค ๋ฒ„ํŠผ: +- ์œ„์น˜: ์ขŒ์ƒ๋‹จ ๋˜๋Š” ์šฐ์ƒ๋‹จ +- ํฌ๊ธฐ: 24px +``` + +#### AI ์ถ”์ฒœ ๋ฐฐ์ง€ + +``` +๋ฐฐ๊ฒฝ: linear-gradient(135deg, #0066FF 0%, #4D94FF 100%) +ํ…์ŠคํŠธ: White +ํฐํŠธ: Body-S (12px SemiBold) +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 12px (pill ํ˜•ํƒœ) +ํŒจ๋”ฉ: 4px 12px +์œ„์น˜: ์นด๋“œ ์ขŒ์ƒ๋‹จ ์ ˆ๋Œ€ ๋ฐฐ์น˜ + +ํ…์ŠคํŠธ: "AI ์ถ”์ฒœ" ๋˜๋Š” "์ธ๊ธฐ" +``` + +### 8.3 Real-time Dashboard Components + +#### KPI Card (ํ•ต์‹ฌ ์ง€ํ‘œ) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๐Ÿ“Š ์ฐธ์—ฌ์ž ์ˆ˜ โ”‚ +โ”‚ โ”‚ +โ”‚ 152๋ช… โ”‚ +โ”‚ +32% โ†‘ ์ „์ฃผ ๋Œ€๋น„ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +๋ฐฐ๊ฒฝ: White +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 16px +๊ทธ๋ฆผ์ž: 0 2px 8px rgba(0,0,0,0.08) +ํŒจ๋”ฉ: 24px + +๊ตฌ์กฐ: +- ์•„์ด์ฝ˜ + ๋ ˆ์ด๋ธ”: Body-S #4A4A4A +- ์ˆ˜์น˜: Display (28px Bold) #1A1A1A +- ๋ณ€ํ™”์œจ: Body-S, ์ฆ๊ฐ€ #00C853, ๊ฐ์†Œ #D32F2F +``` + +#### Chart Container + +``` +๋ฐฐ๊ฒฝ: White +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 12px +๊ทธ๋ฆผ์ž: 0 2px 8px rgba(0,0,0,0.08) +ํŒจ๋”ฉ: 24px + +์ฐจํŠธ ์ƒ‰์ƒ: +- Primary Line: #E31E24 +- Secondary Line: #0066FF +- Grid: #F5F5F5 +- Axis Label: #9E9E9E +``` + +--- + +## 9. ์ธํ„ฐ๋ž™์…˜ ํŒจํ„ด + +### 9.1 Bottom Sheet + +``` +๋ฐฐ๊ฒฝ: White +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 24px 24px 0 0 (์ƒ๋‹จ๋งŒ) +์ตœ๋Œ€ ๋†’์ด: 80vh +Handle: 40px x 4px, #D9D9D9, ์ƒ๋‹จ ์ค‘์•™ +๊ทธ๋ฆผ์ž: 0 -4px 12px rgba(0,0,0,0.15) + +์• ๋‹ˆ๋ฉ”์ด์…˜: +- Open: transform translateY(100% โ†’ 0), 300ms ease-out +- Close: transform translateY(0 โ†’ 100%), 250ms ease-in + +Backdrop: +- ๋ฐฐ๊ฒฝ: rgba(0,0,0,0.5) +- ํด๋ฆญ ์‹œ: Bottom Sheet ๋‹ซํž˜ +``` + +### 9.2 Toast (์•Œ๋ฆผ) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โœ“ ์ด๋ฒคํŠธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +๋ฐฐ๊ฒฝ: #1A1A1A (90% opacity) +ํ…์ŠคํŠธ: White +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 8px +ํŒจ๋”ฉ: 16px 24px +์œ„์น˜: ํ•˜๋‹จ ์ค‘์•™, Bottom Navigation ์œ„ 80px +์ž๋™ ๋‹ซํž˜: 3์ดˆ + +์• ๋‹ˆ๋ฉ”์ด์…˜: +- Show: opacity 0โ†’1, translateY(20pxโ†’0), 200ms +- Hide: opacity 1โ†’0, 200ms + +ํƒ€์ž…๋ณ„ ์•„์ด์ฝ˜: +- Success: โœ“ (#00C853) +- Error: โœ• (#D32F2F) +- Info: โ“˜ (#0288D1) +``` + +### 9.3 Modal (๋ชจ๋‹ฌ ๋‹ค์ด์–ผ๋กœ๊ทธ) + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ H2 ์ œ๋ชฉ [โœ•] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ Body-M ๋ณธ๋ฌธ ๋‚ด์šฉ โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์ทจ์†Œ ๋ฒ„ํŠผ] [ํ™•์ธ ๋ฒ„ํŠผ] โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +๋ฐฐ๊ฒฝ: White +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 16px +์ตœ๋Œ€ ๋„ˆ๋น„: 400px (Mobile), 480px (Tablet+) +ํŒจ๋”ฉ: 24px +๊ทธ๋ฆผ์ž: 0 8px 24px rgba(0,0,0,0.2) + +Backdrop: +- ๋ฐฐ๊ฒฝ: rgba(0,0,0,0.6) +- ํด๋ฆญ ์‹œ: ๋ชจ๋‹ฌ ๋‹ซํž˜ (ํ™•์ธ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ œ์™ธ) + +์• ๋‹ˆ๋ฉ”์ด์…˜: +- Show: opacity 0โ†’1, scale(0.95โ†’1), 250ms ease-out +- Hide: opacity 1โ†’0, scale(1โ†’0.95), 200ms ease-in +``` + +### 9.4 Pull to Refresh + +``` +์ƒํƒœ ํ‘œ์‹œ: +1. Pull Down (๋‹น๊ธฐ๊ธฐ): + - ์•„์ด์ฝ˜: โ†“ ํšŒ์ „ + - ํ…์ŠคํŠธ: "๋‹น๊ฒจ์„œ ์ƒˆ๋กœ๊ณ ์นจ" + +2. Release to Refresh (๋†“์•„์„œ ์ƒˆ๋กœ๊ณ ์นจ): + - ์•„์ด์ฝ˜: โ†ป ํšŒ์ „ + - ํ…์ŠคํŠธ: "๋†“์•„์„œ ์ƒˆ๋กœ๊ณ ์นจ" + +3. Refreshing (์ƒˆ๋กœ๊ณ ์นจ ์ค‘): + - Spinner ํ‘œ์‹œ + - ํ…์ŠคํŠธ: "์ƒˆ๋กœ๊ณ ์นจ ์ค‘..." + +์ƒ‰์ƒ: +- ์•„์ด์ฝ˜/ํ…์ŠคํŠธ: #9E9E9E +- Spinner: #E31E24 + +์œ„์น˜: ํ™”๋ฉด ์ตœ์ƒ๋‹จ +๋†’์ด: 60px +``` + +### 9.5 Swipe Actions (์นด๋“œ ์Šค์™€์ดํ”„) + +``` +์ขŒ์ธก ์Šค์™€์ดํ”„ (์‚ญ์ œ): +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ—€ ์‚ญ์ œ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +๋ฐฐ๊ฒฝ: #D32F2F +์•„์ด์ฝ˜: ๐Ÿ—‘๏ธ White +ํ…์ŠคํŠธ: White Body-M + +์šฐ์ธก ์Šค์™€์ดํ”„ (ํŽธ์ง‘): +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ํŽธ์ง‘ โ–ถ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +๋ฐฐ๊ฒฝ: #0066FF +์•„์ด์ฝ˜: โœ๏ธ White +ํ…์ŠคํŠธ: White Body-M + +Threshold: 30% ๋„ˆ๋น„ ์ด์ƒ ์Šค์™€์ดํ”„ ์‹œ ์•ก์…˜ ํŠธ๋ฆฌ๊ฑฐ +``` + +### 9.6 Drag and Drop + +``` +๋“œ๋ž˜๊ทธ ์ค‘ ์ƒํƒœ: +- ์›๋ณธ ์นด๋“œ: opacity 0.5 +- ๋“œ๋ž˜๊ทธ ์นด๋“œ: ๊ทธ๋ฆผ์ž 0 8px 16px rgba(0,0,0,0.2), scale(1.05) +- Drop Zone: ๋ฐฐ๊ฒฝ #F5F5F5, ํ…Œ๋‘๋ฆฌ 2px dashed #E31E24 + +๋“œ๋กญ ๊ฐ€๋Šฅ ์˜์—ญ: +- ๋ฐฐ๊ฒฝ: #FFF5F5 (light red tint) +- ํ…Œ๋‘๋ฆฌ: 2px dashed #E31E24 +``` + +--- + +## 10. ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ€์ด๋“œ๋ผ์ธ + +### 10.1 Duration (์ง€์† ์‹œ๊ฐ„) + +``` +์ฆ‰์‹œ (Instant): 0ms // ์ƒ‰์ƒ ๋ณ€ํ™” +๋งค์šฐ ๋น ๋ฆ„ (Very Fast): 100ms // Hover ํšจ๊ณผ +๋น ๋ฆ„ (Fast): 200ms // Toast ๋“ฑ์žฅ +์ผ๋ฐ˜ (Normal): 300ms // Modal, Bottom Sheet +๋А๋ฆผ (Slow): 500ms // Page Transition +``` + +### 10.2 Easing (๊ฐ€์†๋„) + +``` +Ease-Out (๊ฐ์†): +- cubic-bezier(0, 0, 0.2, 1) +- ์‚ฌ์šฉ: ์š”์†Œ ๋“ฑ์žฅ (Modal, Sheet, Toast) + +Ease-In (๊ฐ€์†): +- cubic-bezier(0.4, 0, 1, 1) +- ์‚ฌ์šฉ: ์š”์†Œ ํ‡ด์žฅ + +Ease-In-Out (๊ฐ€์†+๊ฐ์†): +- cubic-bezier(0.4, 0, 0.2, 1) +- ์‚ฌ์šฉ: ์ „ํ™˜ ํšจ๊ณผ (Tab ์ „ํ™˜) + +Linear (์ผ์ •): +- linear +- ์‚ฌ์šฉ: ๋ฌดํ•œ ํšŒ์ „ (Spinner, Loading) +``` + +### 10.3 ์ฃผ์š” ์• ๋‹ˆ๋ฉ”์ด์…˜ + +#### Page Transition + +``` +์ง„์ž…: +- opacity: 0 โ†’ 1 +- transform: translateX(20px) โ†’ translateX(0) +- duration: 300ms +- easing: ease-out + +ํ‡ด์žฅ: +- opacity: 1 โ†’ 0 +- transform: translateX(0) โ†’ translateX(-20px) +- duration: 250ms +- easing: ease-in +``` + +#### Card Hover + +``` +transform: translateY(0) โ†’ translateY(-4px) +box-shadow: ์ฆ๊ฐ€ (0 2px 8px โ†’ 0 8px 16px) +duration: 200ms +easing: ease-out +``` + +#### Button Press + +``` +transform: scale(1) โ†’ scale(0.95) +duration: 100ms +easing: ease-out +``` + +--- + +## 11. ์•„์ด์ฝ˜ ์‹œ์Šคํ…œ + +### 11.1 Icon Style + +**Material Icons Outlined** (๊ธฐ๋ณธ) +**Material Icons Filled** (ํ™œ์„ฑํ™” ์ƒํƒœ) + +``` +์Šคํƒ€์ผ ํŠน์ง•: +- Outlined: ์„  ๋‘๊ป˜ 2px, ์‹ฌํ”Œํ•˜๊ณ  ๊น”๋” +- Filled: ํ™œ์„ฑํ™” ์‹œ ์ง๊ด€์  ๊ตฌ๋ถ„ +``` + +### 11.2 Icon Sizes + +``` +Small: 16 x 16px // ํ…์ŠคํŠธ ๋‚ด ์ธ๋ผ์ธ +Medium: 24 x 24px // ๋ฒ„ํŠผ, ๋„ค๋น„๊ฒŒ์ด์…˜ (๊ธฐ๋ณธ) +Large: 32 x 32px // ํ—ค๋” ์•ก์…˜ +XLarge: 48 x 48px // Empty State ์ผ๋Ÿฌ์ŠคํŠธ +``` + +### 11.3 Icon Colors + +``` +Default: #4A4A4A (Gray-700) +Active: #E31E24 (Primary Red) +Disabled: #9E9E9E (Gray-500) +On Color: #FFFFFF (White) - ์ƒ‰์ƒ ๋ฒ„ํŠผ ์œ„ + +AI Feature: #0066FF (AI Blue) +Success: #00C853 +Warning: #FFA000 +Error: #D32F2F +``` + +### 11.4 ์ฃผ์š” ์•„์ด์ฝ˜ ๋งคํ•‘ + +``` +ํ™ˆ: home +์ด๋ฒคํŠธ: event / campaign +๋ถ„์„: analytics / bar_chart +๋งˆ์ด: person / account_circle +์ถ”๊ฐ€: add / add_circle +์•Œ๋ฆผ: notifications +์„ค์ •: settings +๊ฒ€์ƒ‰: search +ํ•„ํ„ฐ: filter_list +์ •๋ ฌ: sort +๊ณต์œ : share +๋‹ค์šด๋กœ๋“œ: download +์—…๋กœ๋“œ: upload +ํŽธ์ง‘: edit +์‚ญ์ œ: delete +๋‹ซ๊ธฐ: close +๋’ค๋กœ: arrow_back +์•ž์œผ๋กœ: arrow_forward +์ฒดํฌ: check / check_circle +์˜ค๋ฅ˜: error / cancel +์ •๋ณด: info +๊ฒฝ๊ณ : warning +๋„์›€๋ง: help / help_outline + +AI ๊ด€๋ จ: +ํŠธ๋ Œ๋“œ: trending_up +๊ฒฝํ’ˆ: card_giftcard +์ด๋ฏธ์ง€: image / photo +์˜์ƒ: videocam +SNS: share / language +QR: qr_code +๋‹ฌ๋ ฅ: calendar_today +์‹œ๊ฐ„: schedule +์œ„์น˜: place +์ฐธ์—ฌ์ž: group / people +``` + +--- + +## 12. ์ ‘๊ทผ์„ฑ ๊ฐ€์ด๋“œ๋ผ์ธ + +### 12.1 ์ƒ‰์ƒ ๋Œ€๋น„ (WCAG 2.1 AA) + +``` +ํ…์ŠคํŠธ: +- ์ผ๋ฐ˜ ํ…์ŠคํŠธ (14px+): 4.5:1 ์ด์ƒ +- ํฐ ํ…์ŠคํŠธ (18px+ or 14px+ Bold): 3:1 ์ด์ƒ + +UI ์š”์†Œ: +- ๋ฒ„ํŠผ, ์ž…๋ ฅ ํ•„๋“œ, ์•„์ด์ฝ˜: 3:1 ์ด์ƒ + +๊ฒ€์ฆ๋œ ์กฐํ•ฉ: +โœ“ #1A1A1A (Black) on #FFFFFF (White): 14.2:1 +โœ“ #4A4A4A (Gray-700) on #FFFFFF: 8.5:1 +โœ“ #E31E24 (KT Red) on #FFFFFF: 7.2:1 +โœ“ #0066FF (AI Blue) on #FFFFFF: 7.8:1 +โœ“ #FFFFFF (White) on #E31E24 (Red): 7.2:1 +โœ— #9E9E9E (Gray-500) on #FFFFFF: 4.6:1 (์ž‘์€ ํ…์ŠคํŠธ ๋ถ€์ ํ•ฉ) +``` + +### 12.2 ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜ + +``` +Tab Order: +- ๋…ผ๋ฆฌ์  ์ˆœ์„œ (์ƒโ†’ํ•˜, ์ขŒโ†’์šฐ) +- ๋ชจ๋“  ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์š”์†Œ ์ ‘๊ทผ ๊ฐ€๋Šฅ + +Focus Indicator: +- ํ…Œ๋‘๋ฆฌ: 2px solid #0066FF +- Offset: 2px +- ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: ๋ฒ„ํŠผ๊ณผ ๋™์ผ + +Focus Trap: +- Modal, Bottom Sheet ์—ด๋ฆผ ์‹œ ๋‚ด๋ถ€๋งŒ ํƒญ ์ด๋™ +- ESC ํ‚ค๋กœ ๋‹ซ๊ธฐ ๊ฐ€๋Šฅ +``` + +### 12.3 ์Šคํฌ๋ฆฐ ๋ฆฌ๋” + +``` +ARIA Labels: +- ๋ชจ๋“  ๋ฒ„ํŠผ์— ๋ช…ํ™•ํ•œ ๋ ˆ์ด๋ธ” +- ์•„์ด์ฝ˜ ๋ฒ„ํŠผ: aria-label="์„ค์ • ์—ด๊ธฐ" +- ์ด๋ฏธ์ง€: alt="AI๊ฐ€ ์ƒ์„ฑํ•œ ํ™๋ณด ์ด๋ฏธ์ง€" + +Role & State: +- role="button", role="dialog" +- aria-expanded="true/false" +- aria-selected="true/false" +- aria-disabled="true/false" + +Live Regions: +- Toast: aria-live="polite" +- Error: aria-live="assertive" +``` + +### 12.4 ํ„ฐ์น˜ ์˜์—ญ + +``` +์ตœ์†Œ ํฌ๊ธฐ: 44 x 44px (WCAG 2.1 AA) +๊ถŒ์žฅ ํฌ๊ธฐ: 48 x 48px + +์ถฉ๋ถ„ํ•œ ๊ฐ„๊ฒฉ: +- ์ธ์ ‘ ํ„ฐ์น˜ ์š”์†Œ ๊ฐ„: 8px ์ด์ƒ +``` + +### 12.5 ๋Œ€์•ˆ ์ œ๊ณต + +``` +์ƒ‰์ƒ๋งŒ์œผ๋กœ ์ •๋ณด ์ „๋‹ฌ ๊ธˆ์ง€: +โœ— ๋‚˜์œ ์˜ˆ: ๋นจ๊ฐ„์ƒ‰๋งŒ์œผ๋กœ ์˜ค๋ฅ˜ ํ‘œ์‹œ +โœ“ ์ข‹์€ ์˜ˆ: ๋นจ๊ฐ„์ƒ‰ + โœ• ์•„์ด์ฝ˜ + "์˜ค๋ฅ˜" ํ…์ŠคํŠธ + +๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ๋Œ€์•ˆ: +- ๋ฒ„ํŠผ ํด๋ฆญ์œผ๋กœ๋„ ์ˆœ์„œ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ +- ํ‚ค๋ณด๋“œ๋กœ ์ˆœ์„œ ์กฐ์ • ๊ฐ€๋Šฅ +``` + +--- + +## 13. ์„ฑ๋Šฅ ์ตœ์ ํ™” + +### 13.1 ์ด๋ฏธ์ง€ ์ตœ์ ํ™” + +``` +ํฌ๋งท: +- ์‚ฌ์ง„: WebP (fallback: JPG) +- ์ผ๋Ÿฌ์ŠคํŠธ: SVG ๋˜๋Š” PNG +- ์•„์ด์ฝ˜: SVG Sprite + +์••์ถ•: +- JPG: Quality 80-85% +- PNG: TinyPNG ๋˜๋Š” ImageOptim + +Lazy Loading: +- ์Šคํฌ๋กค ์‹œ ๋กœ๋“œ (Intersection Observer) +- ์ค‘์š” ์ด๋ฏธ์ง€: eager loading +``` + +### 13.2 ํฐํŠธ ์ตœ์ ํ™” + +``` +Font Loading: +- font-display: swap +- Preload ์ฃผ์š” ํฐํŠธ + +Subsetting: +- ํ•œ๊ธ€: ์ž์ฃผ ์“ฐ๋Š” ๊ธ€์ž๋งŒ (Subset) +- Variable Font ์‚ฌ์šฉ (Pretendard Variable) + +WOFF2 ์šฐ์„ : +@font-face { + font-family: 'Pretendard'; + src: url('/fonts/Pretendard-Variable.woff2') format('woff2-variations'); + font-display: swap; +} +``` + +### 13.3 ์• ๋‹ˆ๋ฉ”์ด์…˜ ์„ฑ๋Šฅ + +``` +GPU ๊ฐ€์† ์‚ฌ์šฉ: +- transform: translate3d(), scale3d() +- opacity +- filter + +ํ”ผํ•ด์•ผ ํ•  ์†์„ฑ: +- width, height (Reflow ๋ฐœ์ƒ) +- top, left (Reflow ๋ฐœ์ƒ) +- background-position + +Will-Change ์‚ฌ์šฉ: +will-change: transform, opacity; +(์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง์ „์—๋งŒ ์ ์šฉ) +``` + +--- + +## 14. ๋‹คํฌ ๋ชจ๋“œ (ํ–ฅํ›„ ์ง€์›) + +### 14.1 ์ƒ‰์ƒ ๋งคํ•‘ (์ฐธ๊ณ ์šฉ) + +``` +Light Mode โ†’ Dark Mode + +๋ฐฐ๊ฒฝ: +#FFFFFF โ†’ #121212 +#F5F5F5 โ†’ #1E1E1E + +ํ…์ŠคํŠธ: +#1A1A1A โ†’ #E0E0E0 +#4A4A4A โ†’ #B0B0B0 +#9E9E9E โ†’ #707070 + +์นด๋“œ: +#FFFFFF โ†’ #1E1E1E +border #E0E0E0 โ†’ #2C2C2C + +Primary (์œ ์ง€): +#E31E24 โ†’ #E31E24 (๋™์ผ) + +Secondary: +#0066FF โ†’ #4D94FF (๋ฐ๊ฒŒ ์กฐ์ •) +``` + +--- + +## 15. ๋ณ€๊ฒฝ ์ด๋ ฅ + +### Version 1.1 (2025-10-21) + +- ํ”„๋กœํ† ํƒ€์ž… ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ ์—…๋ฐ์ดํŠธ +- Editable Field ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (์ธ๋ผ์ธ ํŽธ์ง‘) +- Budget Navigation ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (Sticky ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜) +- Option Card ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€ (์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ ๋ฐฐ์ง€) +- Selection Card ์„ธ๋ถ€์‚ฌํ•ญ ๋ณด์™„ (์ด๋ฏธ์ง€ ๋น„์œจ, z-index) +- ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง ๋ฐฉ์ง€ ๊ฐ€์ด๋“œ ์ถ”๊ฐ€ + +### Version 1.0 (2025-10-17) + +- ์ดˆ์•ˆ ์ž‘์„ฑ +- ๋ธŒ๋žœ๋“œ ์•„์ด๋ดํ‹ฐํ‹ฐ ์ •์˜ +- ๋””์ž์ธ ์›์น™ 5๊ฐ€์ง€ ์ˆ˜๋ฆฝ +- ์ƒ‰์ƒ ์‹œ์Šคํ…œ (Primary/Secondary/Grayscale/Semantic) ์ •์˜ +- ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ ์‹œ์Šคํ…œ (Pretendard, 8๋‹จ๊ณ„ ์Šค์ผ€์ผ) ์ •์˜ +- ๊ฐ„๊ฒฉ ์‹œ์Šคํ…œ (4px ๊ธฐ๋ฐ˜, 6๋‹จ๊ณ„) ์ •์˜ +- ์ปดํฌ๋„ŒํŠธ ์Šคํƒ€์ผ (Button/Card/Input/Navigation) ์ •์˜ +- ๋ฐ˜์‘ํ˜• ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ (Mobile/Tablet/Desktop) ์ •์˜ +- AI ํŠนํ™” ์ปดํฌ๋„ŒํŠธ (๋กœ๋”ฉ/๊ฒฐ๊ณผ/Stepper) ์ •์˜ +- ์ธํ„ฐ๋ž™์…˜ ํŒจํ„ด (BottomSheet/Toast/Modal/Swipe) ์ •์˜ +- ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ€์ด๋“œ๋ผ์ธ ์ •์˜ +- ์•„์ด์ฝ˜ ์‹œ์Šคํ…œ (Material Icons) ์ •์˜ +- ์ ‘๊ทผ์„ฑ ๊ฐ€์ด๋“œ๋ผ์ธ (WCAG 2.1 AA) ์ •์˜ +- ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ฐ€์ด๋“œ ์ •์˜ + +--- + +## 16. ์ฐธ๊ณ  ์ž๋ฃŒ + +- UI/UX ์„ค๊ณ„์„œ: design/uiux/uiux.md +- ์œ ์ €์Šคํ† ๋ฆฌ: design/userstory.md +- KT ์‚ฌ์žฅ๋‹˜Easy: https://product.kt.com/wDic/soho/marketing.do?itemCode=sajangeasy +- wwit.design ๋‹ท์Šฌ๋ž˜์‹œ๋Œ€์‹œ: https://wwit.design/2023/09/30/dotslashdash/ +- Material Design Icons: https://fonts.google.com/icons +- WCAG 2.1 Guidelines: https://www.w3.org/WAI/WCAG21/quickref/ +- Pretendard Font: https://github.com/orioncactus/pretendard + +--- + +**๋ฌธ์„œ ๋** diff --git a/design/uiux/uiux-design.md b/design/uiux/uiux-design.md new file mode 100644 index 0000000..06951dd --- /dev/null +++ b/design/uiux/uiux-design.md @@ -0,0 +1,1002 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - UI/UX ์„ค๊ณ„์„œ + +## ๋ฌธ์„œ ์ •๋ณด + +- ์ž‘์„ฑ์ผ: 2025-10-21 +- ๋ฒ„์ „: 2.0 +- ๊ธฐ๋ฐ˜ ๋ฌธ์„œ: ์œ ์ €์Šคํ† ๋ฆฌ v2.0, ํ”„๋กœํ† ํƒ€์ž… ๋ถ„์„ +- ์„ค๊ณ„ ์›์น™: Mobile First, ์ ‘๊ทผ์„ฑ ์šฐ์„ , ์ผ๊ด€์„ฑ + +--- + +## 1. ์„œ๋น„์Šค ๊ฐœ์š” + +### 1.1 ์„œ๋น„์Šค ๋ชฉ์  + +์†Œ์ƒ๊ณต์ธ์ด AI์˜ ๋„์›€์œผ๋กœ ์ „๋ฌธ ๋งˆ์ผ€ํ„ฐ ์—†์ด๋„ ํšจ๊ณผ์ ์ธ ์ด๋ฒคํŠธ๋ฅผ ๊ธฐํšํ•˜๊ณ  ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์› + +### 1.2 ํ•ต์‹ฌ ๊ฐ€์น˜ + +- **ํ˜์‹ ์„ฑ**: AI ๊ธฐ๋ฐ˜ ์ž๋™ํ™”๋กœ 3๋ถ„ ๋งŒ์— ์ด๋ฒคํŠธ ์ฝ˜ํ…์ธ  ์ƒ์„ฑ +- **์‹ ๋ขฐ์„ฑ**: KT ๋ธŒ๋žœ๋“œ ๊ธฐ๋ฐ˜์˜ ์•ˆ์ •์ ์ธ ์„œ๋น„์Šค +- **์ ‘๊ทผ์„ฑ**: ์ดˆ๋ณด์ž๋„ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ง๊ด€์  ์ธํ„ฐํŽ˜์ด์Šค + +### 1.3 ํƒ€๊ฒŸ ์‚ฌ์šฉ์ž + +- **Primary**: ๋งˆ์ผ€ํŒ… ์ „๋ฌธ ์ง€์‹์ด ๋ถ€์กฑํ•œ ์†Œ์ƒ๊ณต์ธ +- **Secondary**: ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… ๊ฒฝํ—˜์ด ์žˆ๋Š” ์ค‘์†Œ๊ธฐ์—… ๋‹ด๋‹น์ž + +--- + +## 2. ํ™”๋ฉด ๊ตฌ์„ฑ + +### 2.1 ์ „์ฒด ํ™”๋ฉด ๊ตฌ์กฐ (์ด 17๊ฐœ) + +#### ์ธ์ฆ ์˜์—ญ (01~04) + +- **01-๋กœ๊ทธ์ธ.html**: ์ด๋ฉ”์ผ/๋น„๋ฐ€๋ฒˆํ˜ธ ๋กœ๊ทธ์ธ +- **02-ํšŒ์›๊ฐ€์ž….html**: ์‹ ๊ทœ ์‚ฌ์šฉ์ž ๋“ฑ๋ก +- **03-ํ”„๋กœํ•„.html**: ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ๋ฐ ๋งค์žฅ ์ •๋ณด ๊ด€๋ฆฌ +- **04-๋กœ๊ทธ์•„์›ƒํ™•์ธ.html**: ๋กœ๊ทธ์•„์›ƒ ํ™•์ธ ๋ชจ๋‹ฌ + +#### ๋Œ€์‹œ๋ณด๋“œ ์˜์—ญ (05~06) + +- **05-๋Œ€์‹œ๋ณด๋“œ.html**: ๋ฉ”์ธ ํ™ˆ ํ™”๋ฉด + - KPI ์š”์•ฝ (์ง„ํ–‰ ์ค‘ ์ด๋ฒคํŠธ, ์ด ์ฐธ์—ฌ์ž, ํ‰๊ท  ROI) + - ๋น ๋ฅธ ์‹œ์ž‘ (์ƒˆ ์ด๋ฒคํŠธ, ๋ถ„์„) + - ์ง„ํ–‰ ์ค‘์ธ ์ด๋ฒคํŠธ ๋ชฉ๋ก + - ์ตœ๊ทผ ํ™œ๋™ ํƒ€์ž„๋ผ์ธ +- **06-์ด๋ฒคํŠธ๋ชฉ๋ก.html**: ์ „์ฒด ์ด๋ฒคํŠธ ๋ชฉ๋ก ๋ฐ ํ•„ํ„ฐ๋ง + +#### ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ (07~12, 7๋‹จ๊ณ„) + +- **07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ.html**: ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ +- **08-AI์ด๋ฒคํŠธ์ถ”์ฒœ.html**: AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ +- **09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ.html**: SNS ์ด๋ฏธ์ง€ ์Šคํƒ€์ผ ์„ ํƒ +- **10-์ฝ˜ํ…์ธ ํŽธ์ง‘.html**: ํ…์ŠคํŠธ ๋ฐ ์ƒ‰์ƒ ํŽธ์ง‘ +- **11-๋ฐฐํฌ์ฑ„๋„์„ ํƒ.html**: ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ +- **12-์ตœ์ข…์Šน์ธ.html**: ์ด๋ฒคํŠธ ์ตœ์ข… ๊ฒ€ํ†  ๋ฐ ์Šน์ธ + +#### ์ด๋ฒคํŠธ ๊ด€๋ฆฌ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง (13~17) + +- **13-์ด๋ฒคํŠธ์ƒ์„ธ.html**: ์ด๋ฒคํŠธ ์ƒ์„ธ ์ •๋ณด ๋ฐ ์‹ค์‹œ๊ฐ„ KPI +- **14-์ฐธ์—ฌ์ž๋ชฉ๋ก.html**: ์ฐธ์—ฌ์ž ๊ด€๋ฆฌ ๋ฐ ํ•„ํ„ฐ๋ง +- **15-์ด๋ฒคํŠธ์ฐธ์—ฌ.html**: ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ ์ฐธ์—ฌ ํ™”๋ฉด (๊ณ ๊ฐ์šฉ) +- **16-๋‹น์ฒจ์ž์ถ”์ฒจ.html**: ๋‹น์ฒจ์ž ์ถ”์ฒจ ๋ฐ ๊ด€๋ฆฌ +- **17-์„ฑ๊ณผ๋ถ„์„.html**: ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ๋ฐ ์„ฑ๊ณผ ๋ถ„์„ + +### 2.2 ํ™”๋ฉด ํ๋ฆ„๋„ + +``` +[01-๋กœ๊ทธ์ธ / 02-ํšŒ์›๊ฐ€์ž…] + โ†“ +[05-๋Œ€์‹œ๋ณด๋“œ] (Bottom Nav: ํ™ˆ) + โ†“ + โ”œโ”€ [06-์ด๋ฒคํŠธ๋ชฉ๋ก] (Bottom Nav: ์ด๋ฒคํŠธ) + โ”‚ โ†“ + โ”‚ [13-์ด๋ฒคํŠธ์ƒ์„ธ] โ†’ [14-์ฐธ์—ฌ์ž๋ชฉ๋ก] โ†’ [16-๋‹น์ฒจ์ž์ถ”์ฒจ] + โ”‚ โ†“ + โ”‚ [17-์„ฑ๊ณผ๋ถ„์„] (Bottom Nav: ๋ถ„์„) + โ”‚ + โ””โ”€ [์ƒˆ ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ] (FAB) + โ†“ + [07-๋ชฉ์ ์„ ํƒ] + โ†“ + [08-AI์ถ”์ฒœ] + โ†“ + [09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ] + โ†“ + [10-์ฝ˜ํ…์ธ ํŽธ์ง‘] + โ†“ + [11-๋ฐฐํฌ์ฑ„๋„์„ ํƒ] + โ†“ + [12-์ตœ์ข…์Šน์ธ] + โ†“ + [13-์ด๋ฒคํŠธ์ƒ์„ธ] + +[03-ํ”„๋กœํ•„] (Bottom Nav: ํ”„๋กœํ•„) + โ†“ +[04-๋กœ๊ทธ์•„์›ƒํ™•์ธ] +``` + +--- + +## 3. ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ + +### 3.1 Navigation + +#### Bottom Navigation (๋ชจ๋“  ๋ฉ”์ธ ํ™”๋ฉด) + +``` +๊ตฌ์กฐ: 4๊ฐœ ํƒญ +- ํ™ˆ: 05-๋Œ€์‹œ๋ณด๋“œ.html +- ์ด๋ฒคํŠธ: 06-์ด๋ฒคํŠธ๋ชฉ๋ก.html +- ๋ถ„์„: 17-์„ฑ๊ณผ๋ถ„์„.html +- ํ”„๋กœํ•„: 03-ํ”„๋กœํ•„.html + +๋””์ž์ธ: +- ๋†’์ด: 60px +- ๋ฐฐ๊ฒฝ: White +- ๊ทธ๋ฆผ์ž: 0 -2px 8px rgba(0, 0, 0, 0.08) +- ์•„์ด์ฝ˜: Material Icons (24px) +- ํ™œ์„ฑํ™”: KT Red (#E31E24) +- ๋น„ํ™œ์„ฑํ™”: Gray (#9E9E9E) +``` + +#### Header + +``` +๊ตฌ์กฐ: ํƒ€์ดํ‹€ + ๋’ค๋กœ๊ฐ€๊ธฐ + ํ”„๋กœํ•„ +- ๋Œ€์‹œ๋ณด๋“œ: ๋’ค๋กœ๊ฐ€๊ธฐ ์—†์Œ +- ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ: ํ”„๋กœํ•„ ๋ฒ„ํŠผ ์ˆจ๊น€ + +๋””์ž์ธ: +- ๋†’์ด: 56px +- ๋ฐฐ๊ฒฝ: White +- ์ œ๋ชฉ: H1 (24px Bold) +- ์•„์ด์ฝ˜: Material Icons (24px) +``` + +#### FAB (Floating Action Button) + +``` +์œ„์น˜: ์šฐ์ธก ํ•˜๋‹จ ๊ณ ์ • +๊ธฐ๋Šฅ: ์ƒˆ ์ด๋ฒคํŠธ ์ƒ์„ฑ (07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ.html) + +๋””์ž์ธ: +- ํฌ๊ธฐ: 56 x 56px +- ๋ฐฐ๊ฒฝ: KT Red (#E31E24) +- ์•„์ด์ฝ˜: add (White, 24px) +- ๊ทธ๋ฆผ์ž: 0 4px 12px rgba(227, 30, 36, 0.3) +- Bottom Navigation ์œ„ 80px +``` + +### 3.2 Card ์ปดํฌ๋„ŒํŠธ + +#### Default Card + +```css +๋ฐฐ๊ฒฝ: #FFFFFF +ํ…Œ๋‘๋ฆฌ: 1px solid #E0E0E0 +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 12px +๊ทธ๋ฆผ์ž: 0 2px 8px rgba(0, 0, 0, 0.08) +ํŒจ๋”ฉ: 24px +``` + +#### Event Card + +``` +๊ตฌ์กฐ: +- ์ด๋ฒคํŠธ๋ช… (H3) +- ์ƒํƒœ ๋ฐฐ์ง€ (์ง„ํ–‰์ค‘/์˜ˆ์ •/์ข…๋ฃŒ) +- ๊ธฐ๊ฐ„ ์ •๋ณด (Body-S) +- ํ†ต๊ณ„ ์ •๋ณด (์ฐธ์—ฌ์ž, ROI ๋“ฑ) + +์ƒํƒœ: +- Hover: ํ…Œ๋‘๋ฆฌ #E31E24, ๊ทธ๋ฆผ์ž ์ฆ๊ฐ€ +- Selected: ํ…Œ๋‘๋ฆฌ 2px solid #E31E24 +``` + +#### Selection Card (09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ) + +``` +ํŠน์ง•: ์นด๋“œ ์ „์ฒด๊ฐ€ ์„ ํƒ ๊ฐ€๋Šฅํ•œ ์˜์—ญ +- Radio ๋ฒ„ํŠผ ์ˆจ๊น€ (๊ธฐ๋Šฅ์€ ์œ ์ง€) +- ์„ ํƒ ์‹œ ์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ: + - ํ…Œ๋‘๋ฆฌ: 3px solid #E31E24 + - ๊ทธ๋ฆผ์ž: 0 4px 12px rgba(227, 30, 36, 0.2) + - ์šฐ์ธก ์ƒ๋‹จ ์ฒดํฌ ๋ฐฐ์ง€ ํ‘œ์‹œ +- Hover: transform translateY(-2px) +``` + +### 3.3 Form ์ปดํฌ๋„ŒํŠธ + +#### Text Input + +```css +๋†’์ด: 48px +ํŒจ๋”ฉ: 16px +ํ…Œ๋‘๋ฆฌ: 1px solid #D9D9D9 +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 8px +ํฐํŠธ: Body-L (16px Regular) + +Focus: +- ํ…Œ๋‘๋ฆฌ: 2px solid #0066FF +- ๊ทธ๋ฆผ์ž: 0 0 0 4px rgba(0, 102, 255, 0.1) + +Error: +- ํ…Œ๋‘๋ฆฌ: 2px solid #D32F2F +- ๊ทธ๋ฆผ์ž: 0 0 0 4px rgba(211, 47, 47, 0.1) +``` + +#### Checkbox & Radio + +``` +ํฌ๊ธฐ: 24 x 24px (ํ„ฐ์น˜ ์˜์—ญ 44px) + +Checkbox: +- ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 4px +- Checked: ๋ฐฐ๊ฒฝ #E31E24, ์ฒดํฌ๋งˆํฌ White + +Radio: +- ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 50% (์›ํ˜•) +- Selected: ํ…Œ๋‘๋ฆฌ #E31E24, ๋‚ด๋ถ€ ์  12px +``` + +### 3.4 Modal ์ปดํฌ๋„ŒํŠธ + +#### Modal Dialog + +``` +์ตœ๋Œ€ ๋„ˆ๋น„: 400px (Mobile), 480px (Tablet+) +ํŒจ๋”ฉ: 24px +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 16px +๊ทธ๋ฆผ์ž: 0 8px 24px rgba(0, 0, 0, 0.2) + +Backdrop: rgba(0, 0, 0, 0.6) + +์• ๋‹ˆ๋ฉ”์ด์…˜: +- Show: opacity 0โ†’1, scale 0.95โ†’1, 250ms +- Hide: opacity 1โ†’0, scale 1โ†’0.95, 200ms +``` + +#### Bottom Sheet + +``` +์ตœ๋Œ€ ๋†’์ด: 80vh +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 24px 24px 0 0 +Handle: 40px x 4px, #D9D9D9 (์ƒ๋‹จ ์ค‘์•™) + +์• ๋‹ˆ๋ฉ”์ด์…˜: +- Open: translateY(100% โ†’ 0), 300ms +- Close: translateY(0 โ†’ 100%), 250ms +``` + +#### Toast + +``` +์œ„์น˜: ํ•˜๋‹จ ์ค‘์•™, Bottom Nav ์œ„ 80px +๋ฐฐ๊ฒฝ: #1A1A1A (90% opacity) +ํ…์ŠคํŠธ: White +ํŒจ๋”ฉ: 16px 24px +๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 8px +์ž๋™ ๋‹ซํž˜: 3์ดˆ + +ํƒ€์ž…๋ณ„ ์•„์ด์ฝ˜: +- Success: โœ“ (#00C853) +- Error: โœ• (#D32F2F) +- Info: โ“˜ (#0288D1) +``` + +--- + +## 4. ํ™”๋ฉด๋ณ„ ์ƒ์„ธ ์„ค๊ณ„ + +### 4.1 ์ธ์ฆ ์˜์—ญ + +#### 01-๋กœ๊ทธ์ธ.html + +``` +๋ ˆ์ด์•„์›ƒ: +- ๋กœ๊ณ  (์ƒ๋‹จ ์ค‘์•™) +- ๋กœ๊ทธ์ธ ํผ (์ค‘์•™ ์ •๋ ฌ) +- ํšŒ์›๊ฐ€์ž…/๋น„๋ฐ€๋ฒˆํ˜ธ์ฐพ๊ธฐ ๋งํฌ (ํ•˜๋‹จ) + +์ปดํฌ๋„ŒํŠธ: +- Email Input +- Password Input +- "๋กœ๊ทธ์ธ ์œ ์ง€" Checkbox +- Primary Button (๋กœ๊ทธ์ธ) +- Text Button (ํšŒ์›๊ฐ€์ž…, ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ) + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-USER-020 +``` + +#### 02-ํšŒ์›๊ฐ€์ž….html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค +์„น์…˜: +1. ๊ธฐ๋ณธ ์ •๋ณด + - ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ +2. ๋งค์žฅ ์ •๋ณด + - ๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ, ์˜์—…์‹œ๊ฐ„ +3. ์‚ฌ์—…์ž๋ฒˆํ˜ธ (๊ฒ€์ฆ ํ•„์š”) + +์ปดํฌ๋„ŒํŠธ: +- Text Input (๊ฐ ํ•„๋“œ) +- Select Dropdown (์—…์ข…) +- Primary Button (ํšŒ์›๊ฐ€์ž…) +- ๊ฒ€์ฆ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-USER-010 +``` + +#### 03-ํ”„๋กœํ•„.html + +``` +๋ ˆ์ด์•„์›ƒ: +- ํ”„๋กœํ•„ ํ—ค๋” (์ƒ๋‹จ) +- ํŽธ์ง‘ ํผ (์„ธ๋กœ ์Šคํฌ๋กค) +- ์ €์žฅ/๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ (ํ•˜๋‹จ ๊ณ ์ •) + +์„น์…˜: +1. ์‚ฌ์šฉ์ž ์ •๋ณด (์•„์ด์ฝ˜ + ์ด๋ฆ„/์ด๋ฉ”์ผ) +2. ๊ธฐ๋ณธ ์ •๋ณด ํŽธ์ง‘ +3. ๋งค์žฅ ์ •๋ณด ํŽธ์ง‘ +4. ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ +5. ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ + +์ปดํฌ๋„ŒํŠธ: +- Text Input (๊ฐ ํ•„๋“œ) +- Primary Button (์ €์žฅ) +- Text Button (๋กœ๊ทธ์•„์›ƒ, Error ์ƒ‰์ƒ) + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-USER-030 +Bottom Nav: ํ”„๋กœํ•„ ํƒญ ํ™œ์„ฑํ™” +``` + +#### 04-๋กœ๊ทธ์•„์›ƒํ™•์ธ.html + +``` +์œ ํ˜•: Modal Dialog + +๊ตฌ์กฐ: +- ์ œ๋ชฉ: "๋กœ๊ทธ์•„์›ƒ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" +- ๋ณธ๋ฌธ: ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ +- ๋ฒ„ํŠผ: ์ทจ์†Œ (Secondary) / ํ™•์ธ (Primary) + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-USER-040 +``` + +### 4.2 ๋Œ€์‹œ๋ณด๋“œ ์˜์—ญ + +#### 05-๋Œ€์‹œ๋ณด๋“œ.html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +1. KPI ์š”์•ฝ (๊ทธ๋ฆฌ๋“œ) + - ์ง„ํ–‰ ์ค‘ ์ด๋ฒคํŠธ ์ˆ˜ + - ์ด ์ฐธ์—ฌ์ž ์ˆ˜ + - ํ‰๊ท  ROI + +2. ๋น ๋ฅธ ์‹œ์ž‘ (2๊ฐœ ์นด๋“œ) + - ์ƒˆ ์ด๋ฒคํŠธ ๋งŒ๋“ค๊ธฐ + - ๋ถ„์„ ๋ณด๊ธฐ + +3. ์ง„ํ–‰ ์ค‘์ธ ์ด๋ฒคํŠธ (์นด๋“œ ๋ฆฌ์ŠคํŠธ) + - ์ตœ๋Œ€ 5๊ฐœ ํ‘œ์‹œ + - "์ „์ฒด๋ณด๊ธฐ" ๋งํฌ + +4. ์ตœ๊ทผ ํ™œ๋™ (ํƒ€์ž„๋ผ์ธ) + +์ปดํฌ๋„ŒํŠธ: +- KPI Card (3๊ฐœ, ๊ทธ๋ฆฌ๋“œ) +- Action Card (2๊ฐœ, ๊ทธ๋ฆฌ๋“œ) +- Event Card (๋ฆฌ์ŠคํŠธ) +- Timeline Item (๋ฆฌ์ŠคํŠธ) +- FAB (์ƒˆ ์ด๋ฒคํŠธ) + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-EVENT-010 +Bottom Nav: ํ™ˆ ํƒญ ํ™œ์„ฑํ™” +``` + +#### 06-์ด๋ฒคํŠธ๋ชฉ๋ก.html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +1. ๊ฒ€์ƒ‰ ๋ฐ” +2. ํ•„ํ„ฐ (์ƒํƒœ, ๊ธฐ๊ฐ„) +3. ์ •๋ ฌ (์ตœ์‹ ์ˆœ, ์ฐธ์—ฌ์ž์ˆœ, ROI์ˆœ) +4. ์ด๋ฒคํŠธ ์นด๋“œ ๋ฆฌ์ŠคํŠธ +5. ํŽ˜์ด์ง€๋„ค์ด์…˜ + +์ปดํฌ๋„ŒํŠธ: +- Search Input +- Filter Chips +- Dropdown (์ •๋ ฌ) +- Event Card (๋ฆฌ์ŠคํŠธ) +- Pagination + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-EVENT-070 +Bottom Nav: ์ด๋ฒคํŠธ ํƒญ ํ™œ์„ฑํ™” +``` + +### 4.3 ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ + +#### 07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ.html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +- ์ œ๋ชฉ: "์ด๋ฒคํŠธ ๋ชฉ์ ์„ ์„ ํƒํ•˜์„ธ์š”" +- ์˜ต์…˜ ์นด๋“œ (4๊ฐœ) + 1. ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜ + 2. ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ + 3. ๋งค์ถœ ์ฆ๋Œ€ + 4. ์ธ์ง€๋„ ํ–ฅ์ƒ + +์ปดํฌ๋„ŒํŠธ: +- Option Card (4๊ฐœ, Radio ๋ฒ„ํŠผ ํฌํ•จ) + - ์•„์ด์ฝ˜ + ์ œ๋ชฉ + ์„ค๋ช… +- Primary Button (๋‹ค์Œ) + +์ƒํ˜ธ์ž‘์šฉ: +- ์นด๋“œ ์„ ํƒ ์‹œ ๊ฐ•์กฐ ํ‘œ์‹œ +- ์„ ํƒ ํ›„ ๋‹ค์Œ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-EVENT-020 +Progress: 1/7 ๋‹จ๊ณ„ +``` + +#### 08-AI์ด๋ฒคํŠธ์ถ”์ฒœ.html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +1. AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ (์ƒ๋‹จ ์นด๋“œ) + - ์—…์ข… ํŠธ๋ Œ๋“œ (store ์•„์ด์ฝ˜) + - ์ง€์—ญ ํŠธ๋ Œ๋“œ (location_on ์•„์ด์ฝ˜) + - ์‹œ์ฆŒ ํŠธ๋ Œ๋“œ (wb_sunny ์•„์ด์ฝ˜) + +2. ์˜ˆ์‚ฐ๋ณ„ ์ถ”์ฒœ ์ด๋ฒคํŠธ ์ œ๋ชฉ ๋ฐ ์„ค๋ช… + - "์˜ˆ์‚ฐ๋ณ„ ์ถ”์ฒœ ์ด๋ฒคํŠธ" + - "๊ฐ ์˜ˆ์‚ฐ๋ณ„ 2๊ฐ€์ง€ ๋ฐฉ์‹ (์˜จ๋ผ์ธ 1๊ฐœ, ์˜คํ”„๋ผ์ธ 1๊ฐœ)์„ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค" + +3. ์˜ˆ์‚ฐ ์„ ํƒ ๋„ค๋น„๊ฒŒ์ด์…˜ (Sticky) + - ๐Ÿ’ฐ ์ €๋น„์šฉ / ๐Ÿ’ฐ๐Ÿ’ฐ ์ค‘๋น„์šฉ / ๐Ÿ’ฐ๐Ÿ’ฐ๐Ÿ’ฐ ๊ณ ๋น„์šฉ + - ํƒญ ํ˜•ํƒœ, ํด๋ฆญ ์‹œ ํ•ด๋‹น ์„น์…˜์œผ๋กœ ์Šคํฌ๋กค + +4. ์˜ˆ์‚ฐ๋ณ„ ์ถ”์ฒœ ์ด๋ฒคํŠธ (๊ฐ ์˜ˆ์‚ฐ๋‹น 2๊ฐœ ์นด๋“œ) + - ์ €๋น„์šฉ (25~30๋งŒ์›): ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ ๊ฐ 1๊ฐœ + - ์ค‘๋น„์šฉ (60~70๋งŒ์›): ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ ๊ฐ 1๊ฐœ + - ๊ณ ๋น„์šฉ (200~250๋งŒ์›): ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ ๊ฐ 1๊ฐœ + - ๊ฐ ์นด๋“œ ๊ตฌ์„ฑ: + - ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ ๋ฐฐ์ง€ + - ์ด๋ฒคํŠธ ์ œ๋ชฉ (์ธ๋ผ์ธ ํŽธ์ง‘ ๊ฐ€๋Šฅ, editable-field) + - ๊ฒฝํ’ˆ๋ช… (์ธ๋ผ์ธ ํŽธ์ง‘ ๊ฐ€๋Šฅ, editable-field) + - ์ฐธ์—ฌ ๋ฐฉ๋ฒ• + - ์˜ˆ์ƒ ํ†ต๊ณ„ (์ฐธ์—ฌ์ž ์ˆ˜, ๋น„์šฉ, ROI) + - Radio ๋ฒ„ํŠผ (์นด๋“œ ๋‚ด๋ถ€) + +์ปดํฌ๋„ŒํŠธ: +- Trend Analysis Card (3๊ฐœ ํŠธ๋ Œ๋“œ ํ•ญ๋ชฉ) +- Budget Navigation (Sticky, 3๊ฐœ ๋ฒ„ํŠผ) +- Option Card (์ด 6๊ฐœ, Radio ์„ ํƒ) + - ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ ๋ฐฐ์ง€ + - Editable Field (์ œ๋ชฉ, ๊ฒฝํ’ˆ) + - Radio ๋ฒ„ํŠผ (visible) +- Primary Button (๋‹ค์Œ) + +์ƒํ˜ธ์ž‘์šฉ: +- ์˜ˆ์‚ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ ํด๋ฆญ: ํ•ด๋‹น ์„น์…˜์œผ๋กœ smooth scroll +- Editable Field ํด๋ฆญ: ์ธ๋ผ์ธ ํŽธ์ง‘ ํ™œ์„ฑํ™” +- ์ œ๋ชฉ/๊ฒฝํ’ˆ hover: ํŽธ์ง‘ ๊ฐ€๋Šฅ ํ‘œ์‹œ (์ ์„  ํ…Œ๋‘๋ฆฌ) +- Radio ์„ ํƒ: 1๊ฐœ๋งŒ ์„ ํƒ ๊ฐ€๋Šฅ +- ์„ ํƒ ํ›„ ๋‹ค์Œ ๋ฒ„ํŠผ ํ™œ์„ฑํ™” + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-EVENT-030, UFR-AI-010 +Progress: 2/7 ๋‹จ๊ณ„ + +ํŠน์ง•: +- ์˜ˆ์‚ฐ๋ณ„ 2๊ฐ€์ง€ ๋ฐฉ์‹ ์ œ๊ณต (์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ) +- ์ด 6๊ฐœ ์˜ต์…˜ ์ค‘ 1๊ฐœ ์„ ํƒ +- ์ธ๋ผ์ธ ํŽธ์ง‘์œผ๋กœ ์ฆ‰์‹œ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• +``` + +#### 09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ.html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +- ์ œ๋ชฉ: "SNS ์ด๋ฏธ์ง€ ์Šคํƒ€์ผ์„ ์„ ํƒํ•˜์„ธ์š”" +- ์Šคํƒ€์ผ ์นด๋“œ (5๊ฐœ ๊ทธ๋ฆฌ๋“œ) + 1. ์‹ฌํ”Œ - ๊น”๋”ํ•˜๊ณ  ์ฝ๊ธฐ ์‰ฌ์šด ๋””์ž์ธ + 2. ๋ชจ๋˜ - ํŠธ๋ Œ๋””ํ•˜๊ณ  ์„ธ๋ จ๋œ ๋””์ž์ธ + 3. ๊ท€์—ฌ์›€ - ์นœ๊ทผํ•˜๊ณ  ๋”ฐ๋œปํ•œ ๋””์ž์ธ + 4. ๊ณ ๊ธ‰์Šค๋Ÿฌ์›€ - ํ”„๋ฆฌ๋ฏธ์—„ ๋А๋‚Œ์˜ ๋””์ž์ธ + 5. ํŠธ๋ Œ๋”” - MZ์„ธ๋Œ€ ๊ฐ์„ฑ์˜ ๋””์ž์ธ + +์ปดํฌ๋„ŒํŠธ: +- Style Selection Card (5๊ฐœ, ์นด๋“œ ์„ ํƒ UI) + ๊ตฌ์กฐ: + - Selected Badge (์šฐ์ธก ์ƒ๋‹จ, ์กฐ๊ฑด๋ถ€) + - ํฌ๊ธฐ: 32x32px + - ๋ฐฐ๊ฒฝ: KT Red (#E31E24) + - ์•„์ด์ฝ˜: check (White, 20px) + - ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 50% (์›ํ˜•) + - Image Preview (1:1 ๋น„์œจ) + - ๋ฐฐ๊ฒฝ: Gray-100 (ํ”Œ๋ ˆ์ด์Šคํ™€๋”) + - ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ: 12px + - Style Name (H3) + - Description (Body-S, Secondary) + - "ํฌ๊ฒŒ๋ณด๊ธฐ" ๋ฒ„ํŠผ (Secondary Button, Small) + - Radio Input (display: none) + + ์Šคํƒ€์ผ: + - ๊ธฐ๋ณธ: border 3px solid transparent + - Hover: transform translateY(-2px), ๊ทธ๋ฆผ์ž ์ฆ๊ฐ€ + - Selected: border-color KT Red, ๊ทธ๋ฆผ์ž ๊ฐ•์กฐ + - Cursor: pointer + +- Fullscreen Modal + - ๋ฐฐ๊ฒฝ: rgba(0, 0, 0, 0.95) + - Close ๋ฒ„ํŠผ (์šฐ์ธก ์ƒ๋‹จ) + - Image (contain, max 100%) + +- Primary Button (๋‹ค์Œ) + +์ƒํ˜ธ์ž‘์šฉ: +- ์นด๋“œ ํด๋ฆญ: Radio ์„ ํƒ ํ† ๊ธ€ +- Selected ์‹œ: ํ…Œ๋‘๋ฆฌ ๊ฐ•์กฐ + ์ฒดํฌ ๋ฐฐ์ง€ ํ‘œ์‹œ +- "ํฌ๊ฒŒ๋ณด๊ธฐ" ํด๋ฆญ: ์ „์ฒดํ™”๋ฉด ๋ชจ๋‹ฌ (์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง ๋ฐฉ์ง€) +- ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ: ๋ฐฐ๊ฒฝ ํด๋ฆญ or X ๋ฒ„ํŠผ +- AI ์ƒ์„ฑ ์‹œ๊ฐ„: ์•ฝ 5์ดˆ (๋กœ๋”ฉ ํ‘œ์‹œ) + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-CONT-010 +Progress: 3/7 ๋‹จ๊ณ„ + +๊ตฌํ˜„ ํŠน์ง•: +- Radio ๋ฒ„ํŠผ ์ˆจ๊น€ (ํผ ์ œ์ถœ์šฉ์œผ๋กœ๋งŒ ์‚ฌ์šฉ) +- ์นด๋“œ ์ „์ฒด๊ฐ€ label๋กœ ์ž‘๋™ +- ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง ๋ฐฉ์ง€ (stopPropagation) +- ํ‚ค๋ณด๋“œ ์ ‘๊ทผ์„ฑ ์ง€์› (Tab, Enter) +- transition: all 0.3s ease +``` + +#### 10-์ฝ˜ํ…์ธ ํŽธ์ง‘.html + +``` +๋ ˆ์ด์•„์›ƒ: +- Mobile: ์„ธ๋กœ ์Šคํƒ (ํŽธ์ง‘ํผ โ†’ ๋ฏธ๋ฆฌ๋ณด๊ธฐ) +- Desktop: ์ขŒ์šฐ ๋ถ„ํ•  (ํŽธ์ง‘ํผ | ๋ฏธ๋ฆฌ๋ณด๊ธฐ) + +์„น์…˜: +1. ํ…์ŠคํŠธ ํŽธ์ง‘ (์ขŒ์ธก) + - ์ œ๋ชฉ ์ž…๋ ฅ + - ๊ฒฝํ’ˆ ์ž…๋ ฅ + - ์ฐธ์—ฌ ์•ˆ๋‚ด ์ž…๋ ฅ + +2. ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์šฐ์ธก) + - ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ + +์‚ญ์ œ๋œ ๊ธฐ๋Šฅ: +- ๋กœ๊ณ  ์œ„์น˜ ์„ ํƒ (์ƒ๋‹จ/ํ•˜๋‹จ/์ค‘์•™) +- ๋กœ๊ณ  ํฌ๊ธฐ ์กฐ์ • + +์ปดํฌ๋„ŒํŠธ: +- Text Input (์ œ๋ชฉ) +- Text Input (๊ฒฝํ’ˆ) +- Textarea (์ฐธ์—ฌ ์•ˆ๋‚ด) +- Preview Card (์‹ค์‹œ๊ฐ„) +- Primary Button (๋‹ค์Œ) +- Secondary Button (์ €์žฅ) + +์ƒํ˜ธ์ž‘์šฉ: +- ์ž…๋ ฅ ์‹œ ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์—…๋ฐ์ดํŠธ + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-CONT-020 +Progress: 4/7 ๋‹จ๊ณ„ +``` + +#### 11-๋ฐฐํฌ์ฑ„๋„์„ ํƒ.html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +1. ์ฑ„๋„ ์„ ํƒ (๋ณต์ˆ˜ ์„ ํƒ) + - ์šฐ๋ฆฌ๋™๋„คTV (Checkbox) + - ๋ง๊ณ ๋น„์ฆˆ (Checkbox) + - SNS (Checkbox) + - QR์ฝ”๋“œ (Checkbox) + +2. ์ฑ„๋„๋ณ„ ์˜ต์…˜ (์กฐ๊ฑด๋ถ€ ํ‘œ์‹œ) + - ์šฐ๋ฆฌ๋™๋„คTV: ์ง€์—ญ ์„ ํƒ + - SNS: ํ”Œ๋žซํผ ์„ ํƒ (Instagram, Naver, Facebook) + - QR์ฝ”๋“œ: ์ƒ์„ฑ ์˜ต์…˜ + +์ปดํฌ๋„ŒํŠธ: +- Checkbox List (์ฑ„๋„) +- Conditional Options (๊ฐ ์ฑ„๋„) +- Primary Button (๋‹ค์Œ) + +์ƒํ˜ธ์ž‘์šฉ: +- ์ฑ„๋„ ์„ ํƒ ์‹œ ํ•ด๋‹น ์˜ต์…˜ ํ‘œ์‹œ +- ์ตœ์†Œ 1๊ฐœ ์„ ํƒ ํ•„์ˆ˜ + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-DIST-010 +Progress: 5/7 ๋‹จ๊ณ„ +``` + +#### 12-์ตœ์ข…์Šน์ธ.html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +1. ์ด๋ฒคํŠธ ์š”์•ฝ + - ์ œ๋ชฉ, ๊ฒฝํ’ˆ, ์ฐธ์—ฌ ๋ฐฉ๋ฒ• +2. ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ +3. ๋ฐฐํฌ ์ฑ„๋„ +4. ์ผ์ • ์„ค์ • + - ์‹œ์ž‘์ผ: ์ฆ‰์‹œ or ์˜ˆ์•ฝ + - ์ข…๋ฃŒ์ผ: ์„ ํƒ + +์ปดํฌ๋„ŒํŠธ: +- Summary Card (์š”์•ฝ ์ •๋ณด) +- Image Preview +- Channel Badges +- Date Picker (์ผ์ •) +- Primary Button (์Šน์ธ ๋ฐ ๋ฐฐํฌ) +- Secondary Button (์ˆ˜์ •) + +์ƒํ˜ธ์ž‘์šฉ: +- ์ˆ˜์ •: ์ด์ „ ๋‹จ๊ณ„๋กœ ์ด๋™ +- ์Šน์ธ: ์ด๋ฒคํŠธ ์ƒ์„ฑ ๋ฐ ๋ฐฐํฌ + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-DIST-020 +Progress: 7/7 ๋‹จ๊ณ„ +``` + +### 4.4 ์ด๋ฒคํŠธ ๊ด€๋ฆฌ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง + +#### 13-์ด๋ฒคํŠธ์ƒ์„ธ.html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +1. ์ด๋ฒคํŠธ ํ—ค๋” + - ์ด๋ฏธ์ง€ ์ธ๋„ค์ผ + - ์ œ๋ชฉ, ์ƒํƒœ, ๊ธฐ๊ฐ„ + +2. ์‹ค์‹œ๊ฐ„ KPI (4๊ฐœ ์นด๋“œ) + - ์ฐธ์—ฌ์ž ์ˆ˜ + - ์กฐํšŒ์ˆ˜ + - ROI + - ์ „ํ™˜์œจ + +3. ๋น ๋ฅธ ์•ก์…˜ + - ์ฐธ์—ฌ์ž ๋ชฉ๋ก + - ๋‹น์ฒจ์ž ๊ด€๋ฆฌ + - ์„ฑ๊ณผ ๋ถ„์„ + - ์ด๋ฒคํŠธ ์ˆ˜์ • + +4. ์ฐธ์—ฌ ์ถ”์ด ์ฐจํŠธ + +์ปดํฌ๋„ŒํŠธ: +- Event Header (์ด๋ฏธ์ง€ + ์ •๋ณด) +- KPI Card (4๊ฐœ, ๊ทธ๋ฆฌ๋“œ) +- Action Button (4๊ฐœ, ๊ทธ๋ฆฌ๋“œ) +- Chart (์ฐธ์—ฌ ์ถ”์ด) + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-EVENT-060 +Bottom Nav: ์ด๋ฒคํŠธ ํƒญ ํ™œ์„ฑํ™” +``` + +#### 14-์ฐธ์—ฌ์ž๋ชฉ๋ก.html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +1. ๊ฒ€์ƒ‰ ๋ฐ” +2. ํ•„ํ„ฐ (์ฑ„๋„, ์ƒํƒœ) +3. ์ฐธ์—ฌ์ž ์นด๋“œ ๋ฆฌ์ŠคํŠธ +4. ํŽ˜์ด์ง€๋„ค์ด์…˜ +5. ์—‘์…€ ๋‹ค์šด๋กœ๋“œ (Desktop) + +์ปดํฌ๋„ŒํŠธ: +- Search Input +- Filter Dropdowns +- Participant Card (๋ฆฌ์ŠคํŠธ) + - ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ฐธ์—ฌ ๊ฒฝ๋กœ, ์ผ์‹œ, ๋‹น์ฒจ ์—ฌ๋ถ€ +- Pagination +- Download Button (Desktop) + +์ƒํ˜ธ์ž‘์šฉ: +- ์นด๋“œ ํด๋ฆญ: ์ฐธ์—ฌ์ž ์ƒ์„ธ ๋ชจ๋‹ฌ + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-PART-020 +``` + +#### 15-์ด๋ฒคํŠธ์ฐธ์—ฌ.html + +``` +์œ ํ˜•: ๊ณ ๊ฐ์šฉ ํ™”๋ฉด (๊ณต๊ฐœ URL) + +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +1. ์ด๋ฒคํŠธ ์ด๋ฏธ์ง€ +2. ์ด๋ฒคํŠธ ์ •๋ณด + - ์ œ๋ชฉ, ๊ฒฝํ’ˆ, ์ฐธ์—ฌ ๋ฐฉ๋ฒ• +3. ์ฐธ์—ฌ ํผ + - ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ ์ž…๋ ฅ +4. ๊ฐœ์ธ์ •๋ณด ๋™์˜ +5. ์ฐธ์—ฌํ•˜๊ธฐ ๋ฒ„ํŠผ + +์ปดํฌ๋„ŒํŠธ: +- Event Banner (์ด๋ฏธ์ง€) +- Info Card (์ •๋ณด) +- Text Input (์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ) +- Checkbox (๋™์˜) +- Primary Button (์ฐธ์—ฌํ•˜๊ธฐ) + +์ƒํ˜ธ์ž‘์šฉ: +- ์œ ํšจ์„ฑ ๊ฒ€์ฆ +- ์ค‘๋ณต ์ฐธ์—ฌ ๋ฐฉ์ง€ +- ์ฐธ์—ฌ ์™„๋ฃŒ ๋ชจ๋‹ฌ + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-PART-010 +``` + +#### 16-๋‹น์ฒจ์ž์ถ”์ฒจ.html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +1. ์ถ”์ฒจ ์„ค์ • + - ๋‹น์ฒจ์ž ์ˆ˜ ์ž…๋ ฅ + - ์ž๋™ ์ถ”์ฒจ ๋ฒ„ํŠผ + +2. ๋‹น์ฒจ์ž ๋ชฉ๋ก (์ถ”์ฒจ ํ›„) + - ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ๋‹น์ฒจ ๊ฒฝํ’ˆ + +3. ์•ก์…˜ + - ๋ฌธ์ž ๋ฐœ์†ก + - ์—‘์…€ ๋‹ค์šด๋กœ๋“œ + +์ปดํฌ๋„ŒํŠธ: +- Number Input (๋‹น์ฒจ์ž ์ˆ˜) +- Primary Button (์ถ”์ฒจ) +- Winner Card (๋ฆฌ์ŠคํŠธ) +- Action Buttons (๋ฌธ์ž, ๋‹ค์šด๋กœ๋“œ) + +์ƒํ˜ธ์ž‘์šฉ: +- ์ถ”์ฒจ: ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ +- ๊ฒฐ๊ณผ: ์ฆ‰์‹œ ํ‘œ์‹œ + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-PART-030 +``` + +#### 17-์„ฑ๊ณผ๋ถ„์„.html + +``` +๋ ˆ์ด์•„์›ƒ: ์„ธ๋กœ ์Šคํฌ๋กค + +์„น์…˜: +1. ์š”์•ฝ KPI (4๊ฐœ ์นด๋“œ) + - ์ฐธ์—ฌ์ž ์ˆ˜ + - ์ด ๋น„์šฉ + - ์˜ˆ์ƒ ์ˆ˜์ต + - ROI + +2. ์ฐจํŠธ (2๊ฐœ, ๊ทธ๋ฆฌ๋“œ) + - ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ (ํŒŒ์ด ์ฐจํŠธ) + - ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด (๋ผ์ธ ์ฐจํŠธ) + +3. ์ƒ์„ธ ๋ถ„์„ (2๊ฐœ, ๊ทธ๋ฆฌ๋“œ) + - ROI ์ƒ์„ธ (ํ…Œ์ด๋ธ”) + - ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ (๋ฐ” ์ฐจํŠธ) + +์ปดํฌ๋„ŒํŠธ: +- Summary Card (4๊ฐœ, ๊ทธ๋ฆฌ๋“œ) +- Chart Card (ํŒŒ์ด, ๋ผ์ธ) +- Table (ROI ์ƒ์„ธ) +- Bar Chart (์ฐธ์—ฌ์ž ํ”„๋กœํ•„) +- Refresh Indicator (์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ) + +์ƒํ˜ธ์ž‘์šฉ: +- Pull to Refresh (๋ชจ๋ฐ”์ผ) +- 5๋ถ„๋งˆ๋‹ค ์ž๋™ ๊ฐฑ์‹  + +์œ ์ €์Šคํ† ๋ฆฌ: UFR-ANAL-010 +Bottom Nav: ๋ถ„์„ ํƒญ ํ™œ์„ฑํ™” +``` + +--- + +## 5. ๋ฐ˜์‘ํ˜• ๋””์ž์ธ + +### 5.1 Breakpoints + +``` +Mobile (๊ธฐ๋ณธ): 320px ~ 767px +Tablet: 768px ~ 1023px +Desktop: 1024px ์ด์ƒ +``` + +### 5.2 ๋ ˆ์ด์•„์›ƒ ๋ณ€ํ™” + +#### ๋Œ€์‹œ๋ณด๋“œ (05) + +``` +Mobile: KPI ์„ธ๋กœ ์Šคํƒ +Tablet: KPI 2x2 ๊ทธ๋ฆฌ๋“œ, ์ด๋ฒคํŠธ 2์—ด +Desktop: KPI 4์—ด, ์ด๋ฒคํŠธ 3์—ด + ์‚ฌ์ด๋“œ๋ฐ” +``` + +#### ์ด๋ฒคํŠธ ์ƒ์„ฑ (09, 10) + +``` +Mobile: ์„ธ๋กœ ์Šคํƒ +Tablet: ์„ธ๋กœ ์Šคํƒ (๊ฐ„๊ฒฉ ์ฆ๊ฐ€) +Desktop: ์ขŒ์šฐ ๋ถ„ํ•  (ํŽธ์ง‘ | ๋ฏธ๋ฆฌ๋ณด๊ธฐ) +``` + +#### ์„ฑ๊ณผ ๋ถ„์„ (17) + +``` +Mobile: ์ฐจํŠธ ์„ธ๋กœ ์Šคํƒ +Tablet: ์ฐจํŠธ 2x1 ๊ทธ๋ฆฌ๋“œ +Desktop: ์ฐจํŠธ 2x2 ๊ทธ๋ฆฌ๋“œ +``` + +--- + +## 6. ์ธํ„ฐ๋ž™์…˜ ํŒจํ„ด + +### 6.1 Navigation + +- **Bottom Nav**: ํƒญ ํด๋ฆญ ์‹œ ์ฆ‰์‹œ ํ™”๋ฉด ์ „ํ™˜ +- **Back Button**: ์ด์ „ ํ™”๋ฉด์œผ๋กœ ์ด๋™ +- **FAB**: ์ƒˆ ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ ์‹œ์ž‘ + +### 6.2 Form + +- **์‹ค์‹œ๊ฐ„ ๊ฒ€์ฆ**: ์ž…๋ ฅ ํ•„๋“œ blur ์‹œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ +- **์—๋Ÿฌ ๋ฉ”์‹œ์ง€**: ํ•„๋“œ ํ•˜๋‹จ์— ๋นจ๊ฐ„์ƒ‰ ํ…์ŠคํŠธ +- **์„ฑ๊ณต ํ”ผ๋“œ๋ฐฑ**: Toast ๋ฉ”์‹œ์ง€ + +### 6.3 Loading States + +- **AI ์ฒ˜๋ฆฌ**: Progress Indicator + ์˜ˆ์ƒ ์‹œ๊ฐ„ +- **๋ฐ์ดํ„ฐ ๋กœ๋”ฉ**: Skeleton Screen +- **๋ฒ„ํŠผ ํด๋ฆญ**: Spinner + ๋น„ํ™œ์„ฑํ™” + +### 6.4 Feedback + +- **์„ฑ๊ณต**: Green Toast + ์ฒดํฌ ์•„์ด์ฝ˜ +- **์—๋Ÿฌ**: Red Toast + X ์•„์ด์ฝ˜ +- **์ •๋ณด**: Blue Toast + i ์•„์ด์ฝ˜ + +### 6.5 Gestures (Mobile) + +- **Swipe**: ์ด๋ฒคํŠธ ์นด๋“œ ์ขŒ์šฐ ์Šค์™€์ดํ”„ (์‚ญ์ œ/ํŽธ์ง‘) +- **Pull to Refresh**: ๋Œ€์‹œ๋ณด๋“œ, ์„ฑ๊ณผ ๋ถ„์„ +- **Long Press**: ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด ํ‘œ์‹œ + +--- + +## 7. ์ ‘๊ทผ์„ฑ ๊ณ ๋ ค์‚ฌํ•ญ + +### 7.1 WCAG 2.1 AA ์ค€์ˆ˜ + +- **์ƒ‰์ƒ ๋Œ€๋น„**: ์ตœ์†Œ 4.5:1 (ํ…์ŠคํŠธ), 3:1 (UI ์š”์†Œ) +- **ํ„ฐ์น˜ ์˜์—ญ**: ์ตœ์†Œ 44x44px +- **ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜**: ๋ชจ๋“  ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์š”์†Œ ์ ‘๊ทผ ๊ฐ€๋Šฅ +- **Focus Indicator**: ๋ช…ํ™•ํ•œ ํฌ์ปค์Šค ํ‘œ์‹œ + +### 7.2 ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์ง€์› + +- **ARIA Labels**: ๋ชจ๋“  ๋ฒ„ํŠผ, ๋งํฌ, ํผ ํ•„๋“œ +- **ARIA Roles**: ์ ์ ˆํ•œ ์—ญํ•  ์ง€์ • +- **Live Regions**: ๋™์  ์ฝ˜ํ…์ธ  ์—…๋ฐ์ดํŠธ ์•Œ๋ฆผ + +### 7.3 ๋Œ€์•ˆ ์ œ๊ณต + +- **์ƒ‰์ƒ ์™ธ ํ‘œํ˜„**: ์•„์ด์ฝ˜ + ํ…์ŠคํŠธ ์กฐํ•ฉ +- **ํ‚ค๋ณด๋“œ ๋Œ€์•ˆ**: ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ โ†’ ๋ฒ„ํŠผ ํด๋ฆญ +- **์Œ์„ฑ ๋Œ€์•ˆ**: ์ด๋ฏธ์ง€ alt ํ…์ŠคํŠธ + +--- + +## 8. ์„ฑ๋Šฅ ์ตœ์ ํ™” + +### 8.1 ์ด๋ฏธ์ง€ + +- **ํฌ๋งท**: WebP (fallback: JPG) +- **์••์ถ•**: Quality 80-85% +- **Lazy Loading**: ์Šคํฌ๋กค ์‹œ ๋กœ๋“œ + +### 8.2 ํฐํŠธ + +- **Font Display**: swap +- **Preload**: ์ฃผ์š” ํฐํŠธ +- **Subset**: ์ž์ฃผ ์“ฐ๋Š” ๊ธ€์ž๋งŒ + +### 8.3 ์• ๋‹ˆ๋ฉ”์ด์…˜ + +- **GPU ๊ฐ€์†**: transform, opacity +- **Will-Change**: ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง์ „๋งŒ ์ ์šฉ + +--- + +## 9. ์—๋Ÿฌ ์ฒ˜๋ฆฌ + +### 9.1 ๋„คํŠธ์›Œํฌ ์—๋Ÿฌ + +``` +ํ‘œ์‹œ: Toast (๋นจ๊ฐ„์ƒ‰) +๋ฉ”์‹œ์ง€: "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”" +์•ก์…˜: ์žฌ์‹œ๋„ ๋ฒ„ํŠผ +``` + +### 9.2 Validation ์—๋Ÿฌ + +``` +ํ‘œ์‹œ: Input ํ•˜๋‹จ ๋ฉ”์‹œ์ง€ +์ƒ‰์ƒ: Error Red (#D32F2F) +์˜ˆ์‹œ: "์ด๋ฉ”์ผ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค" +``` + +### 9.3 ์„œ๋ฒ„ ์—๋Ÿฌ + +``` +ํ‘œ์‹œ: Modal Dialog +๋ฉ”์‹œ์ง€: "์ผ์‹œ์ ์ธ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”." +์•ก์…˜: ํ™•์ธ ๋ฒ„ํŠผ +``` + +### 9.4 Empty State + +``` +ํ‘œ์‹œ: ์ค‘์•™ ์ •๋ ฌ ๋ฉ”์‹œ์ง€ + ์•„์ด์ฝ˜ +์˜ˆ์‹œ: +- "์•„์ง ์ด๋ฒคํŠธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค" +- "์ฐธ์—ฌ์ž๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค" +์•ก์…˜: ๊ด€๋ จ CTA ๋ฒ„ํŠผ +``` + +--- + +## 10. ๋””์ž์ธ ์‹œ์Šคํ…œ ์ฐธ์กฐ + +- **์Šคํƒ€์ผ ๊ฐ€์ด๋“œ**: design/uiux/style-guide.md +- **์ƒ‰์ƒ ์‹œ์Šคํ…œ**: KT Red (#E31E24), AI Blue (#0066FF) +- **ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ**: Pretendard, 8๋‹จ๊ณ„ ์Šค์ผ€์ผ +- **๊ฐ„๊ฒฉ ์‹œ์Šคํ…œ**: 4px ๊ธฐ๋ฐ˜, 6๋‹จ๊ณ„ +- **์ปดํฌ๋„ŒํŠธ**: Button, Card, Input, Modal, Toast, Bottom Nav +- **์•„์ด์ฝ˜**: Material Icons Outlined/Filled + +--- + +## 11. ๋ณ€๊ฒฝ ์ด๋ ฅ + +### Version 2.1 (2025-10-21) + +- ํ”„๋กœํ† ํƒ€์ž… ์‹ค์ œ ๊ตฌํ˜„ ๋‚ด์šฉ ๋ฐ˜์˜ +- 08-AI์ด๋ฒคํŠธ์ถ”์ฒœ: ์˜ˆ์‚ฐ๋ณ„ 2๊ฐ€์ง€ ๋ฐฉ์‹(์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ) ์ƒ์„ธํ™” +- 08-AI์ด๋ฒคํŠธ์ถ”์ฒœ: Budget Navigation (Sticky) ์ถ”๊ฐ€ +- 08-AI์ด๋ฒคํŠธ์ถ”์ฒœ: Editable Field (์ธ๋ผ์ธ ํŽธ์ง‘) ๊ธฐ๋Šฅ ์ถ”๊ฐ€ +- 09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ: Selection Card ์„ธ๋ถ€ ๊ตฌํ˜„ ์ƒ์„ธ ๊ธฐ์ˆ  +- 09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ: Fullscreen Modal, ์ด๋ฒคํŠธ ๋ฒ„๋ธ”๋ง ๋ฐฉ์ง€ ์ถ”๊ฐ€ + +### Version 2.0 (2025-10-21) + +- ํ”„๋กœํ† ํƒ€์ž… ๋ถ„์„ ๊ธฐ๋ฐ˜ ์ „๋ฉด ๊ฐœ์ • +- ํ™”๋ฉด ๋ฒˆํ˜ธ ์ฒด๊ณ„ ์ •๋ฆฝ (01~17๋ฒˆ) +- ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ตฌ์กฐ ๋ณ€๊ฒฝ (ํ–„๋ฒ„๊ฑฐ ๋ฉ”๋‰ด ์ œ๊ฑฐ, Bottom Nav 4ํƒญ) +- ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ ์ƒ์„ธํ™” (7๋‹จ๊ณ„) +- ์นด๋“œ ์„ ํƒ UI ํŒจํ„ด ์ถ”๊ฐ€ (09-์ฝ˜ํ…์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ) +- ๋กœ๊ณ  ์œ„์น˜ ์„ ํƒ ์‚ญ์ œ (10-์ฝ˜ํ…์ธ ํŽธ์ง‘) +- ์„ฑ๊ณผ ๋ถ„์„ Bottom Nav ์ถ”๊ฐ€ (17-์„ฑ๊ณผ๋ถ„์„) +- ์‹ค์‹œ๊ฐ„ KPI ์—…๋ฐ์ดํŠธ ์ถ”๊ฐ€ (13-์ด๋ฒคํŠธ์ƒ์„ธ) + +### Version 1.0 (2025-10-17) + +- ์ดˆ์•ˆ ์ž‘์„ฑ + +--- + +**๋ฌธ์„œ ๋** diff --git a/design/uiux/uiux.md b/design/uiux/uiux.md new file mode 100644 index 0000000..7207ab8 --- /dev/null +++ b/design/uiux/uiux.md @@ -0,0 +1,2473 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค UI/UX ์„ค๊ณ„์„œ + +## ๋ฌธ์„œ ์ •๋ณด +- **ํ”„๋กœ์ ํŠธ๋ช…**: KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค +- **์ž‘์„ฑ์ผ**: 2025-10-21 +- **๋ฒ„์ „**: 1.0 +- **์ž‘์„ฑ์ž**: UI/UX Designer + +--- + +## ๋ชฉ์ฐจ +1. [ํ”„๋กœ์ ํŠธ ๊ฐœ์š”](#1-ํ”„๋กœ์ ํŠธ-๊ฐœ์š”) +2. [์„ค๊ณ„ ์›์น™](#2-์„ค๊ณ„-์›์น™) +3. [ํ”„๋กœํ† ํƒ€์ž… ํ™”๋ฉด ๋ชฉ๋ก](#3-ํ”„๋กœํ† ํƒ€์ž…-ํ™”๋ฉด-๋ชฉ๋ก) +4. [ํ™”๋ฉด ๊ฐ„ ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ](#4-ํ™”๋ฉด-๊ฐ„-์‚ฌ์šฉ์ž-ํ”Œ๋กœ์šฐ) +5. [ํ™”๋ฉด๋ณ„ ์ƒ์„ธ ์„ค๊ณ„](#5-ํ™”๋ฉด๋ณ„-์ƒ์„ธ-์„ค๊ณ„) +6. [ํ™”๋ฉด ๊ฐ„ ์ „ํ™˜ ๋ฐ ๋„ค๋น„๊ฒŒ์ด์…˜](#6-ํ™”๋ฉด-๊ฐ„-์ „ํ™˜-๋ฐ-๋„ค๋น„๊ฒŒ์ด์…˜) +7. [๋ฐ˜์‘ํ˜• ์„ค๊ณ„ ์ „๋žต](#7-๋ฐ˜์‘ํ˜•-์„ค๊ณ„-์ „๋žต) +8. [์ ‘๊ทผ์„ฑ ๋ณด์žฅ ๋ฐฉ์•ˆ](#8-์ ‘๊ทผ์„ฑ-๋ณด์žฅ-๋ฐฉ์•ˆ) +9. [์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐฉ์•ˆ](#9-์„ฑ๋Šฅ-์ตœ์ ํ™”-๋ฐฉ์•ˆ) +10. [๋ณ€๊ฒฝ ์ด๋ ฅ](#10-๋ณ€๊ฒฝ-์ด๋ ฅ) + +--- + +## 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” + +### 1.1 ์„œ๋น„์Šค ๋ชฉ์  +์†Œ์ƒ๊ณต์ธ ๋ฐ ์ค‘์†Œ๊ธฐ์—…์˜ ์ด๋ฒคํŠธ ๊ธฐํšยท์ œ์ž‘ยท์šด์˜ ์—ญ๋Ÿ‰๊ณผ ์‹œ๊ฐ„ ๋ถ€์กฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ AI ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค + +### 1.2 ํ•ต์‹ฌ ๊ฐ€์น˜ ์ œ์•ˆ +- **AI ํŠธ๋ Œ๋“œ ๋ถ„์„**: ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ ์ž๋™ ๋ถ„์„ +- **3๊ฐ€์ง€ ์ด๋ฒคํŠธ ์ถ”์ฒœ**: ๋ชฉ์ ์— ๋งž๋Š” ์ตœ์ ํ™”๋œ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ +- **์ž๋™ ์ฝ˜ํ…์ธ  ์ƒ์„ฑ**: SNS ์ด๋ฏธ์ง€ 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์ž๋™ ์ƒ์„ฑ +- **๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ**: ์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS ๋™์‹œ ๋ฐฐํฌ +- **์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ ์ธก์ •**: ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ๋กœ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  ๋ถ„์„ + +### 1.3 ํƒ€๊ฒŸ ์‚ฌ์šฉ์ž +- **์ฃผ ์‚ฌ์šฉ์ž**: ์†Œ์ƒ๊ณต์ธ (์Œ์‹์ , ์นดํŽ˜, ์†Œ๋งค์  ์šด์˜์ž) +- **์‚ฌ์šฉ ํ™˜๊ฒฝ**: ๋ชจ๋ฐ”์ผ ์ค‘์‹ฌ (60%), ๋ฐ์Šคํฌํ†ฑ (40%) +- **์‚ฌ์šฉ ์‹œ๊ฐ„**: ๋งค์žฅ ์šด์˜ ์ค‘ ์งง์€ ์‹œ๊ฐ„ ํ™œ์šฉ (5-10๋ถ„ ๋‚ด ์ด๋ฒคํŠธ ์ƒ์„ฑ) + +### 1.4 ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ตฌ์„ฑ +1. **User** - ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๋งค์žฅ์ •๋ณด ๊ด€๋ฆฌ +2. **Event** - ์ด๋ฒคํŠธ ๊ธฐํš ๋ฐ ๊ด€๋ฆฌ +3. **AI** - AI ๊ธฐ๋ฐ˜ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ +4. **Content** - SNS ์ฝ˜ํ…์ธ  ์ƒ์„ฑ +5. **Distribution** - ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ ๊ด€๋ฆฌ +6. **Participation** - ์ด๋ฒคํŠธ ์ฐธ์—ฌ ๋ฐ ๋‹น์ฒจ์ž ๊ด€๋ฆฌ +7. **Analytics** - ์‹ค์‹œ๊ฐ„ ํšจ๊ณผ ์ธก์ • ๋ฐ ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ + +--- + +## 2. ์„ค๊ณ„ ์›์น™ + +### 2.1 Mobile First ๋””์ž์ธ ์ฒ ํ•™ + +#### ์šฐ์„ ์ˆœ์œ„ ์ค‘์‹ฌ ์„ค๊ณ„ (Priority-Driven Design) +- **Above the Fold ์ตœ์ ํ™”**: ์Šคํฌ๋กค ์—†์ด ํ•ต์‹ฌ ์ •๋ณด ํ™•์ธ +- **Single Column Layout**: ๋ชจ๋ฐ”์ผ์—์„œ ๋‹จ์ผ ์ปฌ๋Ÿผ์œผ๋กœ ์ •๋ณด ์ˆ˜์ง ๋ฐฐ์น˜ +- **Progressive Disclosure**: ์ƒ์„ธ ์ •๋ณด๋Š” ํƒญ/์•„์ฝ”๋””์–ธ์œผ๋กœ ์ˆจ๊น€ + +#### ์ ์ง„์  ํ–ฅ์ƒ (Progressive Enhancement) +``` +Mobile (320-767px) โ†’ ํ•ต์‹ฌ ๊ธฐ๋Šฅ๋งŒ ์ œ๊ณต +Tablet (768-1023px) โ†’ ๋ถ€๊ฐ€ ์ •๋ณด ์ถ”๊ฐ€ +Desktop (1024px+) โ†’ ์ „์ฒด ๊ธฐ๋Šฅ ๋ฐ ์ƒ์„ธ ๋ถ„์„ +``` + +#### ์„ฑ๋Šฅ ์ตœ์ ํ™” (Performance Optimization) +- **First Contentful Paint**: < 1.5s +- **Time to Interactive**: < 3s +- **Lighthouse Score**: > 90 + +### 2.2 ์œ ์ €์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ์„ค๊ณ„ +- **๋ถˆํ•„์š”ํ•œ ์ถ”๊ฐ€ ์„ค๊ณ„ ๊ธˆ์ง€**: 20๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ์™€ ์ •ํ™•ํžˆ ๋งค์นญ +- **์šฐ์„ ์ˆœ์œ„ ๋ฐ˜์˜**: M(Must) > S(Should) > C(Could) > W(Won't) ์ˆœ์„œ๋กœ ๊ฐœ๋ฐœ + +### 2.3 ์‚ฌ์šฉ์„ฑ ์›์น™ +- **3 Tap Rule**: ๋ชจ๋“  ์ฃผ์š” ๊ธฐ๋Šฅ์€ 3๋ฒˆ ํƒญ ๋‚ด ๋„๋‹ฌ ๊ฐ€๋Šฅ +- **ํ„ฐ์น˜ ์˜์—ญ**: ์ตœ์†Œ 44x44px (์• ํ”Œ ๊ถŒ์žฅ) +- **๊ฐ€๋…์„ฑ**: ์ตœ์†Œ ๊ธ€๊ผด ํฌ๊ธฐ 14px (๋ชจ๋ฐ”์ผ), 16px (๋ฐ์Šคํฌํ†ฑ) +- **์ƒ‰์ƒ ๋Œ€๋น„**: WCAG AA ๋“ฑ๊ธ‰ ์ด์ƒ (4.5:1) + +--- + +## 3. ํ”„๋กœํ† ํƒ€์ž… ํ™”๋ฉด ๋ชฉ๋ก + +### ์ด 17๊ฐœ ํ™”๋ฉด + +| No | ํ™”๋ฉด๋ช… | ์œ ์ €์Šคํ† ๋ฆฌ | ์šฐ์„ ์ˆœ์œ„ | ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค | Mobile ์ค‘์š”๋„ | +|----|--------|----------|---------|--------------|-------------| +| **์ธ์ฆ ๋ฐ ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ** | +| 01 | ๋กœ๊ทธ์ธ | UFR-USER-020 | M/8 | User | โญโญโญ | +| 02 | ํšŒ์›๊ฐ€์ž… | UFR-USER-010 | M/21 | User | โญโญโญ | +| 03 | ํ”„๋กœํ•„ํŽธ์ง‘ | UFR-USER-030 | C/8 | User | โญ | +| 04 | ๋กœ๊ทธ์•„์›ƒํ™•์ธ | UFR-USER-040 | S/3 | User | โญ | +| **๋ฉ”์ธ ๋ฐ ๋Œ€์‹œ๋ณด๋“œ** | +| 05 | ๋Œ€์‹œ๋ณด๋“œ | UFR-EVENT-010 | S/13 | Event | โญโญโญ | +| 06 | ์ด๋ฒคํŠธ๋ชฉ๋ก | UFR-EVENT-070 | S/13 | Event | โญโญ | +| **์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ** | +| 07 | ์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ | UFR-EVENT-020 | M/5 | Event | โญโญโญ | +| 08 | AI์ด๋ฒคํŠธ์ถ”์ฒœ | UFR-EVENT-030 | M/34 | AI | โญโญโญ | +| 09 | SNS์ด๋ฏธ์ง€์ƒ์„ฑ | UFR-CONT-010 | M/21 | Content | โญโญโญ | +| 10 | ์ฝ˜ํ…์ธ ํŽธ์ง‘ | UFR-CONT-020 | S/13 | Content | โญโญ | +| 11 | ๋ฐฐํฌ์ฑ„๋„์„ ํƒ | UFR-EVENT-040 | M/13 | Event | โญโญ | +| 12 | ์ตœ์ข…์Šน์ธ | UFR-EVENT-050 | M/13 | Event | โญโญโญ | +| **์ด๋ฒคํŠธ ๊ด€๋ฆฌ** | +| 13 | ์ด๋ฒคํŠธ์ƒ์„ธ | UFR-EVENT-060 | S/13 | Event | โญโญโญ | +| 14 | ์ฐธ์—ฌ์ž๋ชฉ๋ก | UFR-PART-020 | S/8 | Participation | โญ | +| **์ฐธ์—ฌ์ž ๊ด€๋ฆฌ** | +| 15 | ์ด๋ฒคํŠธ์ฐธ์—ฌ | UFR-PART-010 | M/13 | Participation | โญโญโญ | +| 16 | ๋‹น์ฒจ์ž์ถ”์ฒจ | UFR-PART-030 | M/13 | Participation | โญ | +| **์„ฑ๊ณผ ๋ถ„์„** | +| 17 | ์‹ค์‹œ๊ฐ„๋Œ€์‹œ๋ณด๋“œ | UFR-ANAL-010 | M/34 | Analytics | โญโญ | + +--- + +## 4. ํ™”๋ฉด ๊ฐ„ ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ + +```mermaid +graph TD + Start([์‹œ์ž‘]) --> Login{๋กœ๊ทธ์ธ ์—ฌ๋ถ€} + + %% ์ตœ์ดˆ ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ + Login -->|๋ฏธ๋กœ๊ทธ์ธ| SignUp[02-ํšŒ์›๊ฐ€์ž…] + SignUp --> Login01[01-๋กœ๊ทธ์ธ] + Login01 --> Dashboard[05-๋Œ€์‹œ๋ณด๋“œ] + + Login -->|๋กœ๊ทธ์ธ๋จ| Dashboard + + %% ๋ฉ”์ธ ๋Œ€์‹œ๋ณด๋“œ ์•ก์…˜ + Dashboard --> EventList[06-์ด๋ฒคํŠธ๋ชฉ๋ก] + Dashboard --> EventDetail[13-์ด๋ฒคํŠธ์ƒ์„ธ] + Dashboard --> Profile[03-ํ”„๋กœํ•„ํŽธ์ง‘] + Dashboard --> CreateEvent[07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ] + Dashboard --> Logout[04-๋กœ๊ทธ์•„์›ƒํ™•์ธ] + + %% ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ (ํ•ต์‹ฌ) + CreateEvent --> AIRecommend[08-AI์ด๋ฒคํŠธ์ถ”์ฒœ] + AIRecommend --> ImageGen[09-SNS์ด๋ฏธ์ง€์ƒ์„ฑ] + ImageGen --> ContentEdit[10-์ฝ˜ํ…์ธ ํŽธ์ง‘] + ContentEdit --> ChannelSelect[11-๋ฐฐํฌ์ฑ„๋„์„ ํƒ] + ChannelSelect --> FinalApproval[12-์ตœ์ข…์Šน์ธ] + FinalApproval --> Dashboard + + %% ์ด๋ฒคํŠธ ๊ด€๋ฆฌ ํ”Œ๋กœ์šฐ + EventDetail --> ParticipantList[14-์ฐธ์—ฌ์ž๋ชฉ๋ก] + EventDetail --> DrawWinner[16-๋‹น์ฒจ์ž์ถ”์ฒจ] + EventDetail --> Analytics[17-์‹ค์‹œ๊ฐ„๋Œ€์‹œ๋ณด๋“œ] + + EventList --> EventDetail + + %% ๊ณ ๊ฐ ์ฐธ์—ฌ ํ”Œ๋กœ์šฐ (๋ณ„๋„) + External([์™ธ๋ถ€ ์ฑ„๋„
SNS/TV/์—ฐ๊ฒฐ์Œ]) --> Participate[15-์ด๋ฒคํŠธ์ฐธ์—ฌ] + Participate --> ThankYou([์ฐธ์—ฌ ์™„๋ฃŒ]) + + %% ํ”„๋กœํ•„ ํŽธ์ง‘ + Profile --> Dashboard + + %% ๋กœ๊ทธ์•„์›ƒ + Logout --> Login01 + + style Dashboard fill:#4A90E2,color:#fff + style CreateEvent fill:#7ED321,color:#fff + style AIRecommend fill:#7ED321,color:#fff + style FinalApproval fill:#7ED321,color:#fff + style Analytics fill:#F5A623,color:#fff + style Participate fill:#BD10E0,color:#fff +``` + +--- + +## 5. ํ™”๋ฉด๋ณ„ ์ƒ์„ธ ์„ค๊ณ„ + +### 5.1 ์ธ์ฆ ๋ฐ ์‚ฌ์šฉ์ž ๊ด€๋ฆฌ + +#### 01-๋กœ๊ทธ์ธ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ๊ธฐ์กด ์‚ฌ์šฉ์ž์˜ ์•ˆ์ „ํ•œ ์„œ๋น„์Šค ์ ‘๊ทผ +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-USER-020 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: M/8 (ํ•„์ˆ˜, ๋‚ฎ์€ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ์ „ํ™”๋ฒˆํ˜ธ/๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ +- ๋กœ๊ทธ์ธ ์œ ์ง€ ์˜ต์…˜ +- ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ +- ํšŒ์›๊ฐ€์ž… ๋งํฌ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ”‚ +โ”‚ [์„œ๋น„์Šค ๋กœ๊ณ ] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ „ํ™”๋ฒˆํ˜ธ ์ž…๋ ฅ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ˜ ๋กœ๊ทธ์ธ ์œ ์ง€ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ | ํšŒ์›๊ฐ€์ž… โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Desktop (1024px+)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ”‚ +โ”‚ [์„œ๋น„์Šค ๋กœ๊ณ  + ์„ค๋ช…] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ „ํ™”๋ฒˆํ˜ธ ์ž…๋ ฅ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ˜ ๋กœ๊ทธ์ธ ์œ ์ง€ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ | ํšŒ์›๊ฐ€์ž… โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **ํฌ์ปค์Šค ์ด๋™**: Enter ํ‚ค๋กœ ๋‹ค์Œ ์ž…๋ ฅ ํ•„๋“œ ์ด๋™ +2. **์ž…๋ ฅ ๊ฒ€์ฆ**: ์‹ค์‹œ๊ฐ„ ํ˜•์‹ ๊ฒ€์ฆ (์ „ํ™”๋ฒˆํ˜ธ ํ˜•์‹, ๋น„๋ฐ€๋ฒˆํ˜ธ ์ตœ์†Œ ๊ธธ์ด) +3. **๋น„๋ฐ€๋ฒˆํ˜ธ ํ‘œ์‹œ**: ๋ˆˆ ์•„์ด์ฝ˜ ํด๋ฆญ์œผ๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด๊ธฐ/์ˆจ๊ธฐ๊ธฐ +4. **๋กœ๊ทธ์ธ ์‹คํŒจ**: ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ž…๋ ฅ ํ•„๋“œ ํ•˜๋‹จ์— ๋นจ๊ฐ„์ƒ‰์œผ๋กœ ํ‘œ์‹œ +5. **๋กœ๋”ฉ ์ƒํƒœ**: ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” + ์Šคํ”ผ๋„ˆ ํ‘œ์‹œ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์„ธ๋กœ ์ „์ฒด ํ™”๋ฉด, ๋ฒ„ํŠผ ํ•˜๋‹จ ๊ณ ์ • +- Tablet: ์ค‘์•™ ์นด๋“œ ํ˜•ํƒœ (์ตœ๋Œ€ ๋„ˆ๋น„ 480px) +- Desktop: ์ขŒ์ธก ๋ธŒ๋žœ๋”ฉ + ์šฐ์ธก ๋กœ๊ทธ์ธ ํผ (50:50 ๋ ˆ์ด์•„์›ƒ) + +**์ ‘๊ทผ์„ฑ** +- Label๊ณผ Input ์—ฐ๊ฒฐ (for/id ์†์„ฑ) +- ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋Š” aria-live="assertive" +- Tab ํ‚ค ์ˆœ์„œ: ์ „ํ™”๋ฒˆํ˜ธ โ†’ ๋น„๋ฐ€๋ฒˆํ˜ธ โ†’ ๋กœ๊ทธ์ธ ์œ ์ง€ โ†’ ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ + +--- + +#### 02-ํšŒ์›๊ฐ€์ž… + +**๊ฐœ์š”** +- **๋ชฉ์ **: ์‹ ๊ทœ ์†Œ์ƒ๊ณต์ธ ์‚ฌ์šฉ์ž ์˜จ๋ณด๋”ฉ +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-USER-010 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: M/21 (ํ•„์ˆ˜, ์ค‘๊ฐ„ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ๋‹ค๋‹จ๊ณ„ ํผ (3๋‹จ๊ณ„) + - Step 1: ๊ธฐ๋ณธ ์ •๋ณด (์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ) + - Step 2: ๋งค์žฅ ์ •๋ณด (๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ, ์˜์—…์‹œ๊ฐ„) + - Step 3: ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ +- ์ง„ํ–‰ ์ธ๋””์ผ€์ดํ„ฐ +- ๋‹จ๊ณ„๋ณ„ ์œ ํšจ์„ฑ ๊ฒ€์ฆ +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ตญ์„ธ์ฒญ DB ์—ฐ๋™ ๊ฒ€์ฆ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px) - Step 1* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ† ํšŒ์›๊ฐ€์ž… โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ—โ”€โ”€โ—‹โ”€โ”€โ—‹ (์ง„ํ–‰๋ฐ”) โ”‚ +โ”‚ โ”‚ +โ”‚ ๊ธฐ๋ณธ ์ •๋ณด ์ž…๋ ฅ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ด๋ฆ„ * โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ „ํ™”๋ฒˆํ˜ธ * โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ด๋ฉ”์ผ * โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋น„๋ฐ€๋ฒˆํ˜ธ * โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ * โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ, โ”‚ +โ”‚ ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž โ”‚ +โ”‚ ํฌํ•จ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋‹ค์Œ ๋‹จ๊ณ„ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Mobile (320-767px) - Step 2* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ† ํšŒ์›๊ฐ€์ž… โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ—‹โ”€โ”€โ—โ”€โ”€โ—‹ (์ง„ํ–‰๋ฐ”) โ”‚ +โ”‚ โ”‚ +โ”‚ ๋งค์žฅ ์ •๋ณด ์ž…๋ ฅ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋งค์žฅ๋ช… * โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์—…์ข… ์„ ํƒ * โ”‚ โ”‚ (๋“œ๋กญ๋‹ค์šด) +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ฃผ์†Œ ๊ฒ€์ƒ‰ * โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ƒ์„ธ ์ฃผ์†Œ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์˜์—…์‹œ๊ฐ„ โ”‚ โ”‚ (์„ ํƒ) +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋‹ค์Œ ๋‹จ๊ณ„ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Mobile (320-767px) - Step 3* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ† ํšŒ์›๊ฐ€์ž… โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ—‹โ”€โ”€โ—‹โ”€โ”€โ— (์ง„ํ–‰๋ฐ”) โ”‚ +โ”‚ โ”‚ +โ”‚ ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์‚ฌ์—…์ž๋ฒˆํ˜ธ * โ”‚ โ”‚ +โ”‚ โ”‚ 000-00-00000 โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๊ฒ€์ฆํ•˜๊ธฐ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ [๊ฒ€์ฆ ๊ฒฐ๊ณผ ์˜์—ญ] โ”‚ +โ”‚ โœ“ ์ •์ƒ ์‚ฌ์—…์ž โ”‚ +โ”‚ ์—…์ฒด๋ช…: ์™•๊ฐˆ๋น„ํ†ต๋‹ญ โ”‚ +โ”‚ ๋Œ€ํ‘œ์ž: ํ™๊ธธ๋™ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํšŒ์›๊ฐ€์ž… ์™„๋ฃŒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **์ง„ํ–‰๋ฐ”**: ํ˜„์žฌ ๋‹จ๊ณ„ ํ‘œ์‹œ, ํด๋ฆญ์œผ๋กœ ์ด์ „ ๋‹จ๊ณ„ ์ด๋™ ๊ฐ€๋Šฅ +2. **ํ•„์ˆ˜ ํ•„๋“œ**: ๋ณ„ํ‘œ(*) ํ‘œ์‹œ, ๋ฏธ์ž…๋ ฅ ์‹œ ๋‹ค์Œ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” +3. **์‹ค์‹œ๊ฐ„ ๊ฒ€์ฆ**: + - ์ „ํ™”๋ฒˆํ˜ธ: 010-0000-0000 ํ˜•์‹ ์ž๋™ ํฌ๋งท + - ์ด๋ฉ”์ผ: @ ํฌํ•จ ์—ฌ๋ถ€ ํ™•์ธ + - ๋น„๋ฐ€๋ฒˆํ˜ธ: 8์ž ์ด์ƒ, ์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ ์—ฌ๋ถ€ ํ‘œ์‹œ +4. **์ฃผ์†Œ ๊ฒ€์ƒ‰**: Daum ์ฃผ์†Œ API ํŒ์—… ๋˜๋Š” Bottom Sheet +5. **์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ**: + - ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ (3์ดˆ ์ด๋‚ด) + - ์„ฑ๊ณต: ๋…น์ƒ‰ ์ฒดํฌ + ์—…์ฒด ์ •๋ณด ํ‘œ์‹œ + - ์‹คํŒจ: ๋นจ๊ฐ„์ƒ‰ X + ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ +6. **๋’ค๋กœ ๊ฐ€๊ธฐ**: ์ด์ „ ์ž…๋ ฅ ๊ฐ’ ์œ ์ง€ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์ „์ฒด ํ™”๋ฉด, ๋‹จ๊ณ„๋ณ„ ์Šคํฌ๋กค +- Tablet: ์ค‘์•™ ์นด๋“œ (์ตœ๋Œ€ ๋„ˆ๋น„ 600px) +- Desktop: ์ขŒ์ธก ์ง„ํ–‰๋ฐ” + ์šฐ์ธก ํผ (30:70 ๋ ˆ์ด์•„์›ƒ) + +**์ ‘๊ทผ์„ฑ** +- ์ง„ํ–‰๋ฐ”๋Š” aria-label="ํšŒ์›๊ฐ€์ž… ์ง„ํ–‰ ๋‹จ๊ณ„" +- ํ•„์ˆ˜ ํ•„๋“œ๋Š” aria-required="true" +- ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋Š” aria-live="polite" + +--- + +#### 03-ํ”„๋กœํ•„ํŽธ์ง‘ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ์‚ฌ์šฉ์ž ์ •๋ณด ์ˆ˜์ • +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-USER-030 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: C/8 (์„ ํƒ, ๋‚ฎ์€ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ๊ธฐ๋ณธ ์ •๋ณด ์ˆ˜์ • (์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ) +- ๋งค์žฅ ์ •๋ณด ์ˆ˜์ • (๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ, ์˜์—…์‹œ๊ฐ„) +- ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ (ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ํ•„์ˆ˜) +- ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ €์žฅ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ํ”„๋กœํ•„ ํŽธ์ง‘ ๐Ÿ‘คโ”‚ โ† Header ์ถ”๊ฐ€ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ–ผ ๊ธฐ๋ณธ ์ •๋ณด โ”‚ +โ”‚ ์ด๋ฆ„: [ํ™๊ธธ๋™ ] โ”‚ +โ”‚ ์ „ํ™”๋ฒˆํ˜ธ: [010-...] โ”‚ +โ”‚ ์ด๋ฉ”์ผ: [hong@...]โ”‚ +โ”‚ โ”‚ +โ”‚ โ–ผ ๋งค์žฅ ์ •๋ณด โ”‚ +โ”‚ ๋งค์žฅ๋ช…: [์™•๊ฐˆ๋น„...]โ”‚ +โ”‚ ์—…์ข…: [์Œ์‹์  ] โ”‚ +โ”‚ ์ฃผ์†Œ: [์ˆ˜์›์‹œ...] โ”‚ +โ”‚ ์˜์—…์‹œ๊ฐ„: [10:00~]โ”‚ +โ”‚ โ”‚ +โ”‚ โ–ผ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ โ”‚ +โ”‚ ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ: [ ] โ”‚ +โ”‚ ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ: [ ] โ”‚ +โ”‚ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ: [ ] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ €์žฅํ•˜๊ธฐ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„โ”‚ โ† Nav Bottom ์ถ”๊ฐ€ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **์•„์ฝ”๋””์–ธ**: ๊ฐ ์„น์…˜ ํด๋ฆญ์œผ๋กœ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ +2. **์ „ํ™”๋ฒˆํ˜ธ ๋ณ€๊ฒฝ**: ์žฌ์ธ์ฆ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ (SMS ์ธ์ฆ) +3. **์ €์žฅ ํ™•์ธ**: "๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ €์žฅํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" ๋‹ค์ด์–ผ๋กœ๊ทธ +4. **์ทจ์†Œ ํ™•์ธ**: ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์„ ๊ฒฝ์šฐ "์ €์žฅํ•˜์ง€ ์•Š๊ณ  ๋‚˜๊ฐ€์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" ๋‹ค์ด์–ผ๋กœ๊ทธ +5. **์„ฑ๊ณต ํ† ์ŠคํŠธ**: "ํ”„๋กœํ•„์ด ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" (2์ดˆ) + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์ „์ฒด ํ™”๋ฉด, ์•„์ฝ”๋””์–ธ ํ˜•ํƒœ +- Tablet/Desktop: ์ค‘์•™ ์นด๋“œ (์ตœ๋Œ€ ๋„ˆ๋น„ 720px), ์ €์žฅ ๋ฒ„ํŠผ ํ•˜๋‹จ ๊ณ ์ • + +--- + +#### 04-๋กœ๊ทธ์•„์›ƒํ™•์ธ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ์•ˆ์ „ํ•œ ๋กœ๊ทธ์•„์›ƒ +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-USER-040 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: S/3 (๊ถŒ์žฅ, ๋‚ฎ์€ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ๋กœ๊ทธ์•„์›ƒ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ +- ์„ธ์…˜ ์ข…๋ฃŒ +- ๋กœ๊ทธ์ธ ํ™”๋ฉด ์ด๋™ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ”‚ +โ”‚ [์–ด๋‘์šด ๋ฐฐ๊ฒฝ] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋กœ๊ทธ์•„์›ƒ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ๋กœ๊ทธ์•„์›ƒ ํ•˜์‹œ๊ฒ  โ”‚ โ”‚ +โ”‚ โ”‚ ์Šต๋‹ˆ๊นŒ? โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚์ทจ์†Œ โ”‚โ”‚ํ™•์ธ โ”‚โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ**: ํ”„๋กœํ•„ ๋ฉ”๋‰ด โ†’ ๋กœ๊ทธ์•„์›ƒ ์„ ํƒ +2. **ํ™•์ธ**: ์„ธ์…˜ ์ข…๋ฃŒ + ๋กœ๊ทธ์ธ ํ™”๋ฉด ์ด๋™ (์• ๋‹ˆ๋ฉ”์ด์…˜) +3. **์ทจ์†Œ**: ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‹ซ๊ธฐ, ์ด์ „ ํ™”๋ฉด ์œ ์ง€ +4. **๋ฐฐ๊ฒฝ ํด๋ฆญ**: ์ทจ์†Œ์™€ ๋™์ผ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: Bottom Sheet ํ˜•ํƒœ +- Tablet/Desktop: ์ค‘์•™ Modal Dialog (์ตœ๋Œ€ ๋„ˆ๋น„ 400px) + +--- + +### 5.2 ๋ฉ”์ธ ๋ฐ ๋Œ€์‹œ๋ณด๋“œ + +#### 05-๋Œ€์‹œ๋ณด๋“œ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ๋ฉ”์ธ ํ—ˆ๋ธŒ, ์ด๋ฒคํŠธ ํ˜„ํ™ฉ ํ•œ๋ˆˆ์— ํŒŒ์•… +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-EVENT-010 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: S/13 (๊ถŒ์žฅ, ์ค‘๊ฐ„ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ์ง„ํ–‰์ค‘ ์ด๋ฒคํŠธ 3๊ฐœ ํ‘œ์‹œ (๋ชจ๋ฐ”์ผ) +- ์˜ˆ์ •/์ข…๋ฃŒ ์ด๋ฒคํŠธ ๊ฐœ์ˆ˜ +- "์ƒˆ ์ด๋ฒคํŠธ ๋งŒ๋“ค๊ธฐ" ๋ฒ„ํŠผ (FAB) +- Bottom Navigation (ํ™ˆ, ์ด๋ฒคํŠธ, ๋ถ„์„, ํ”„๋กœํ•„) + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ๋Œ€์‹œ๋ณด๋“œ ๐Ÿ‘ค โ”‚ โ† Header +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ์•ˆ๋…•ํ•˜์„ธ์š”, ํ™๊ธธ๋™๋‹˜!โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ง„ํ–‰์ค‘ 3 | ์˜ˆ์ • 1โ”‚ โ”‚ +โ”‚ โ”‚ ์ข…๋ฃŒ 5 | ์ด 9๊ฐœ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ์ง„ํ–‰์ค‘ ์ด๋ฒคํŠธ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์‹ ๊ทœ๊ณ ๊ฐ ์ด๋ฒคํŠธ โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ์ž: 128๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ D-5 ์ข…๋ฃŒ๊นŒ์ง€ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์žฌ๋ฐฉ๋ฌธ ์ด๋ฒคํŠธ โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ์ž: 56๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ D-12 ์ข…๋ฃŒ๊นŒ์ง€ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋งค์ถœ์ฆ๋Œ€ ์ด๋ฒคํŠธ โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ์ž: 89๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ D-20 ์ข…๋ฃŒ๊นŒ์ง€ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๋”๋ณด๊ธฐ > โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„ โ”‚ โ† Nav Bottom +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + [+] โ† FAB +``` + +*Desktop (1024px+)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋กœ๊ณ ] ๋Œ€์‹œ๋ณด๋“œ [ํ”„๋กœํ•„] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์‚ฌ์ด๋“œ๋ฐ”] ์•ˆ๋…•ํ•˜์„ธ์š”, ํ™๊ธธ๋™๋‹˜! โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ€ข ๋Œ€์‹œ๋ณด๋“œ โ”‚ ์ง„ํ–‰์ค‘ 5 | ์˜ˆ์ • 2 | ์ข…๋ฃŒ 10 โ”‚โ”‚ +โ”‚ โ€ข ์ด๋ฒคํŠธ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ€ข ๋ถ„์„ โ”‚ +โ”‚ โ€ข ํ”„๋กœํ•„ ์ง„ํ–‰์ค‘ ์ด๋ฒคํŠธ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ”‚์ด๋ฒคํŠธ1โ”‚โ”‚์ด๋ฒคํŠธ2โ”‚โ”‚์ด๋ฒคํŠธ3โ”‚โ”‚ +โ”‚ โ”‚์ฐธ์—ฌ128โ”‚โ”‚์ฐธ์—ฌ56โ”‚โ”‚์ฐธ์—ฌ89โ”‚โ”‚ +โ”‚ โ”‚D-5 โ”‚โ”‚D-12 โ”‚โ”‚D-20 โ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚์ด๋ฒคํŠธ4โ”‚โ”‚์ด๋ฒคํŠธ5โ”‚ โ”‚ +โ”‚ โ”‚... โ”‚โ”‚... โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ์˜ˆ์ • ์ด๋ฒคํŠธ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚์ด๋ฒคํŠธ1โ”‚โ”‚์ด๋ฒคํŠธ2โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **์ด๋ฒคํŠธ ์นด๋“œ ํด๋ฆญ**: ์ด๋ฒคํŠธ ์ƒ์„ธ ํ™”๋ฉด ์ด๋™ +2. **FAB ํด๋ฆญ**: ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ ํ™”๋ฉด ์ด๋™ +3. **๋”๋ณด๊ธฐ**: ์ด๋ฒคํŠธ ๋ชฉ๋ก ํ™”๋ฉด ์ด๋™ +4. **Bottom Navigation**: ๋ฉ”๋‰ด ์ „ํ™˜ (์• ๋‹ˆ๋ฉ”์ด์…˜) + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์นด๋“œ 1์—ด, Bottom Navigation +- Tablet: ์นด๋“œ 2์—ด, Bottom Navigation +- Desktop: ์นด๋“œ 3์—ด, Side Navigation + +--- + +#### 06-์ด๋ฒคํŠธ๋ชฉ๋ก + +**๊ฐœ์š”** +- **๋ชฉ์ **: ์ „์ฒด ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ ๋ฐ ๊ด€๋ฆฌ +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-EVENT-070 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: S/13 (๊ถŒ์žฅ, ์ค‘๊ฐ„ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ์ „์ฒด ์ด๋ฒคํŠธ ๋ชฉ๋ก ํ…Œ์ด๋ธ” +- ์ƒํƒœ๋ณ„ ํ•„ํ„ฐ (์ „์ฒด/์ง„ํ–‰์ค‘/์˜ˆ์ •/์ข…๋ฃŒ) +- ๊ธฐ๊ฐ„๋ณ„ ํ•„ํ„ฐ (์ตœ๊ทผ 1๊ฐœ์›”/3๊ฐœ์›”/6๊ฐœ์›”/1๋…„/์ „์ฒด) +- ์ด๋ฒคํŠธ๋ช… ๊ฒ€์ƒ‰ +- ์ •๋ ฌ ๊ธฐ๋Šฅ (์ตœ์‹ ์ˆœ/์ฐธ์—ฌ์ž์ˆœ/ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ ์ˆœ) + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ์ด๋ฒคํŠธ ๋ชฉ๋ก ๐Ÿ‘คโ”‚ โ† Header +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [๊ฒ€์ƒ‰์ฐฝ] โ”‚ +โ”‚ ๐Ÿ” ์ด๋ฒคํŠธ๋ช… ๊ฒ€์ƒ‰... โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“‚ ํ•„ํ„ฐ โ”‚ +โ”‚ [์ „์ฒดโ–ผ] [์ตœ๊ทผ1๊ฐœ์›”โ–ผ]โ”‚ +โ”‚ โ”‚ +โ”‚ ์ •๋ ฌ: [์ตœ์‹ ์ˆœ โ–ผ] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์‹ ๊ทœ๊ณ ๊ฐ ์ด๋ฒคํŠธ โ”‚ โ”‚ +โ”‚ โ”‚ ์ง„ํ–‰์ค‘ | D-5 โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ 128๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : 450%โ”‚ โ”‚ +โ”‚ โ”‚ 2025-11-01~15 โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์žฌ๋ฐฉ๋ฌธ ์ด๋ฒคํŠธ โ”‚ โ”‚ +โ”‚ โ”‚ ์ง„ํ–‰์ค‘ | D-12 โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ 56๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : 320%โ”‚ โ”‚ +โ”‚ โ”‚ 2025-11-05~20 โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋งค์ถœ์ฆ๋Œ€ ์ด๋ฒคํŠธ โ”‚ โ”‚ +โ”‚ โ”‚ ์ข…๋ฃŒ โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ 234๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : 580%โ”‚ โ”‚ +โ”‚ โ”‚ 2025-10-15~31 โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ [๋”๋ณด๊ธฐ...] โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„โ”‚ โ† Nav Bottom +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Desktop (1024px+)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋กœ๊ณ ] ์ด๋ฒคํŠธ ๋ชฉ๋ก [ํ”„๋กœํ•„] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์‚ฌ์ด๋“œ๋ฐ”] ๐Ÿ” [๊ฒ€์ƒ‰์ฐฝ] ๐Ÿ“‚ ํ•„ํ„ฐโ”‚ +โ”‚ ์ด๋ฒคํŠธ๋ช… ๊ฒ€์ƒ‰... [์ „์ฒดโ–ผ] โ”‚ +โ”‚ โ€ข ๋Œ€์‹œ๋ณด๋“œ [์ตœ๊ทผ1๊ฐœ์›”โ–ผ]โ”‚ +โ”‚ โ€ข ์ด๋ฒคํŠธ ์ •๋ ฌ: [์ตœ์‹ ์ˆœ โ–ผ] โ”‚ +โ”‚ โ€ข ๋ถ„์„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ +โ”‚ โ€ข ํ”„๋กœํ•„ โ”‚์ด๋ฒคํŠธ๋ช…โ”‚๊ธฐ๊ฐ„โ”‚์ƒํƒœโ”‚์ฐธ์—ฌ์žโ”‚ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚์‹ ๊ทœ๊ณ ๊ฐโ”‚11/1~โ”‚์ง„ํ–‰โ”‚128๋ช…โ”‚450% โ”‚ +โ”‚ โ”‚์ด๋ฒคํŠธ โ”‚11/15โ”‚์ค‘ โ”‚ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚์žฌ๋ฐฉ๋ฌธ โ”‚11/5~โ”‚์ง„ํ–‰โ”‚56๋ช… โ”‚320% โ”‚ +โ”‚ โ”‚์ด๋ฒคํŠธ โ”‚11/20โ”‚์ค‘ โ”‚ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚๋งค์ถœ์ฆ๋Œ€โ”‚10/15โ”‚์ข…๋ฃŒโ”‚234๋ช…โ”‚580% โ”‚ +โ”‚ โ”‚์ด๋ฒคํŠธ โ”‚~10/31โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚... โ”‚... โ”‚... โ”‚... โ”‚... โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”˜ +โ”‚ โ”‚ +โ”‚ [1] 2 3 4 5 ... 10 > โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **๊ฒ€์ƒ‰**: ์ž…๋ ฅ ์ค‘ ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—…๋ฐ์ดํŠธ +2. **ํ•„ํ„ฐ ์ ์šฉ**: ์ƒํƒœ/๊ธฐ๊ฐ„ ์„ ํƒ ์‹œ ์ฆ‰์‹œ ๋ชฉ๋ก ๊ฐฑ์‹  +3. **์ •๋ ฌ**: ๋“œ๋กญ๋‹ค์šด ์„ ํƒ ์‹œ ์ •๋ ฌ ๊ธฐ์ค€ ๋ณ€๊ฒฝ +4. **์นด๋“œ/ํ–‰ ํด๋ฆญ**: ์ด๋ฒคํŠธ ์ƒ์„ธ ํ™”๋ฉด์œผ๋กœ ์ด๋™ +5. **ํŽ˜์ด์ง€๋„ค์ด์…˜**: ํŽ˜์ด์ง€๋‹น 20๊ฐœ ํ‘œ์‹œ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์นด๋“œ ํ˜•ํƒœ 1์—ด ํ‘œ์‹œ +- Tablet: ์นด๋“œ ํ˜•ํƒœ 2์—ด ํ‘œ์‹œ +- Desktop: ํ…Œ์ด๋ธ” ํ˜•ํƒœ ํ‘œ์‹œ + +--- + +### 5.3 ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ + +#### 07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ์œผ๋กœ AI ์ถ”์ฒœ ์ตœ์ ํ™” +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-EVENT-020 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: M/5 (ํ•„์ˆ˜, ๋‚ฎ์€ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- 4๊ฐ€์ง€ ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ +- ๊ฐ ๋ชฉ์ ๋ณ„ ์„ค๋ช… ๋ฐ ์˜ˆ์ƒ ํšจ๊ณผ ์ œ๊ณต +- ๋ชฉ์  ์„ ํƒ ํ›„ AI ์ถ”์ฒœ ์ž๋™ ์ง„ํ–‰ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ ๐Ÿ‘คโ”‚ โ† Header +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ์ด๋ฒคํŠธ ๋ชฉ์ ์„ ์„ ํƒํ•ด โ”‚ +โ”‚ ์ฃผ์„ธ์š” โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ—‹ ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ ์ƒˆ๋กœ์šด ๊ณ ๊ฐ ํ™•๋ณด โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: ์ฐธ์—ฌ์œจ ๋†’์Œโ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ—‹ ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ โ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ ๊ธฐ์กด ๊ณ ๊ฐ ์žฌ๋ฐฉ๋ฌธ โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: ์ถฉ์„ฑ๋„ ํ–ฅ์ƒโ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ—‹ ๋งค์ถœ ์ฆ๋Œ€ โ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ ๋‹จ๊ธฐ ๋งค์ถœ ํ–ฅ์ƒ โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: ์ฆ‰๊ฐ์  ํšจ๊ณผโ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ—‹ ์ธ์ง€๋„ ํ–ฅ์ƒ โ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ ๋ธŒ๋žœ๋“œ ์ธ์ง€๋„ ์ œ๊ณ โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: ์žฅ๊ธฐ์  ํšจ๊ณผโ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋‹ค์Œ ๋‹จ๊ณ„ โ”‚ โ”‚ โ† ์„ ํƒ ํ›„ ํ™œ์„ฑํ™” +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„ โ”‚ โ† Nav Bottom +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Tablet/Desktop (768px+)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ† ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ์ด๋ฒคํŠธ ๋ชฉ์ ์„ ์„ ํƒํ•ด์ฃผ์„ธ์š” โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚โ—‹์‹ ๊ทœ๊ณ ๊ฐ โ”‚โ”‚โ—‹์žฌ๋ฐฉ๋ฌธ โ”‚ โ”‚ +โ”‚ โ”‚ ์œ ์น˜ โ”‚โ”‚ ์œ ๋„ โ”‚ โ”‚ +โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚์ƒˆ๋กœ์šด ๊ณ ๊ฐโ”‚โ”‚๊ธฐ์กด ๊ณ ๊ฐ โ”‚ โ”‚ +โ”‚ โ”‚ํ™•๋ณด โ”‚โ”‚์žฌ๋ฐฉ๋ฌธ โ”‚ โ”‚ +โ”‚ โ”‚์ฐธ์—ฌ์œจ ๋†’์Œโ”‚โ”‚์ถฉ์„ฑ๋„ ํ–ฅ์ƒโ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚โ—‹๋งค์ถœ โ”‚โ”‚โ—‹์ธ์ง€๋„ โ”‚ โ”‚ +โ”‚ โ”‚ ์ฆ๋Œ€ โ”‚โ”‚ ํ–ฅ์ƒ โ”‚ โ”‚ +โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚๋‹จ๊ธฐ ๋งค์ถœ โ”‚โ”‚๋ธŒ๋žœ๋“œ โ”‚ โ”‚ +โ”‚ โ”‚ํ–ฅ์ƒ โ”‚โ”‚์ธ์ง€๋„ ์ œ๊ณ โ”‚ โ”‚ +โ”‚ โ”‚์ฆ‰๊ฐ์ ํšจ๊ณผโ”‚โ”‚์žฅ๊ธฐ์ ํšจ๊ณผโ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋‹ค์Œ ๋‹จ๊ณ„ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **๋ชฉ์  ์„ ํƒ**: ๋ผ๋””์˜ค ๋ฒ„ํŠผ, 1๊ฐœ๋งŒ ์„ ํƒ ๊ฐ€๋Šฅ +2. **์นด๋“œ ํด๋ฆญ**: ์ „์ฒด ์นด๋“œ ์˜์—ญ ํด๋ฆญ์œผ๋กœ ์„ ํƒ +3. **๋‹ค์Œ ๋‹จ๊ณ„**: ์„ ํƒ ํ›„ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”, AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ํ™”๋ฉด ์ด๋™ +4. **์• ๋‹ˆ๋ฉ”์ด์…˜**: ์„ ํƒ ์‹œ ์นด๋“œ ํ•˜์ด๋ผ์ดํŠธ ํšจ๊ณผ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์นด๋“œ 1์—ด ์„ธ๋กœ ๋ฐฐ์น˜ +- Tablet/Desktop: ์นด๋“œ 2ร—2 ๊ทธ๋ฆฌ๋“œ ๋ฐฐ์น˜ + +--- + +#### 08-AI์ด๋ฒคํŠธ์ถ”์ฒœ + +**๊ฐœ์š”** +- **๋ชฉ์ **: AI ํŠธ๋ Œ๋“œ ๋ถ„์„ + 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ์ถ”์ฒœ +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-EVENT-030 (ํ†ตํ•ฉ) +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: M/34 (ํ•„์ˆ˜, ์ตœ๊ณ  ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ ํ‘œ์‹œ (์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ) +- 3๊ฐ€์ง€ ์˜ˆ์‚ฐ ์ˆ˜์ค€๋ณ„ ์ด๋ฒคํŠธ ์ถ”์ฒœ (์ €๋น„์šฉ/์ค‘๋น„์šฉ/๊ณ ๋น„์šฉ) +- ๊ฐ ์˜ˆ์‚ฐ๋ณ„ ์˜จ/์˜คํ”„๋ผ์ธ ๋ฐฉ์‹ 2๊ฐ€์ง€ ์ถ”์ฒœ (์˜จ๋ผ์ธ 1๊ฐœ, ์˜คํ”„๋ผ์ธ 1๊ฐœ) +- ์ œ๋ชฉ/๊ฒฝํ’ˆ ๊ฐ„ํŽธ ์ˆ˜์ • (์ธ๋ผ์ธ ํŽธ์ง‘) +- ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ (10์ดˆ ์ด๋‚ด) + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px) - ๋กœ๋”ฉ ์ƒํƒœ* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ๐Ÿ‘คโ”‚ โ† Header ์ถ”๊ฐ€ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”‚ [AI ์•„์ด์ฝ˜ ์• ๋‹ˆ๋ฉ”์ด์…˜]โ”‚ +โ”‚ โ”‚ +โ”‚ AI๊ฐ€ ํŠธ๋ Œ๋“œ๋ฅผ ๋ถ„์„ํ•˜๊ณ โ”‚ +โ”‚ ์ตœ์ ์˜ ์ด๋ฒคํŠธ๋ฅผ โ”‚ +โ”‚ ์ถ”์ฒœํ•˜๊ณ  ์žˆ์–ด์š”... โ”‚ +โ”‚ โ”‚ +โ”‚ [์ง„ํ–‰ ๋ฐ” 50%] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„โ”‚ โ† Nav Bottom ์ถ”๊ฐ€ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Mobile (320-767px) - ์™„๋ฃŒ ์ƒํƒœ* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ๐Ÿ‘คโ”‚ โ† Header +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ–ผ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ โ”‚ +โ”‚ (์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ) โ”‚ +โ”‚ โ”‚ +โ”‚ ์˜ˆ์‚ฐ๋ณ„ ์ถ”์ฒœ ์ด๋ฒคํŠธ โ”‚ +โ”‚ (๊ฐ ์˜ˆ์‚ฐ๋ณ„ 2๊ฐ€์ง€ ๋ฐฉ์‹)โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ’ฐ ์˜ต์…˜ 1: ์ €๋น„์šฉโ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ ๐ŸŒ ์˜จ๋ผ์ธ ๋ฐฉ์‹ โ”‚ โ”‚ +โ”‚ โ”‚ [SNS ํŒ”๋กœ์šฐ..โœ] โ”‚ โ”‚ โ† ์ œ๋ชฉ ์ธ๋ผ์ธ ํŽธ์ง‘ +โ”‚ โ”‚ ๊ฒฝํ’ˆ: [์ปคํ”ผ ์ฟ ํฐโœ]โ”‚ โ”‚ โ† ๊ฒฝํ’ˆ ์ธ๋ผ์ธ ํŽธ์ง‘ +โ”‚ โ”‚ ์ฐธ์—ฌ: SNS ํŒ”๋กœ์šฐ โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: 180๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ ๋น„์šฉ: 25๋งŒ์› โ”‚ โ”‚ +โ”‚ โ”‚ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : 520%โ”‚ โ”‚ +โ”‚ โ”‚ โ—‹ ์„ ํƒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿช ์˜คํ”„๋ผ์ธ ๋ฐฉ์‹ โ”‚ โ”‚ +โ”‚ โ”‚ [์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก..โœ]โ”‚ โ”‚ +โ”‚ โ”‚ ๊ฒฝํ’ˆ: [์ปคํ”ผ ์ฟ ํฐโœ]โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ: ์ „ํ™”๋ฒˆํ˜ธ โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: 150๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ ๋น„์šฉ: 30๋งŒ์› โ”‚ โ”‚ +โ”‚ โ”‚ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : 450%โ”‚ โ”‚ +โ”‚ โ”‚ โ—‹ ์„ ํƒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ’ณ ์˜ต์…˜ 2: ์ค‘๋น„์šฉโ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ ๐ŸŒ ์˜จ๋ผ์ธ ๋ฐฉ์‹ โ”‚ โ”‚ +โ”‚ โ”‚ [์ธ์Šคํƒ€ ๋Œ“๊ธ€..โœ]โ”‚ โ”‚ +โ”‚ โ”‚ ๊ฒฝํ’ˆ: [์ƒํ’ˆ๊ถŒ1๋งŒโœ]โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ: SNS ๋Œ“๊ธ€ โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: 250๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ ๋น„์šฉ: 48๋งŒ์› โ”‚ โ”‚ +โ”‚ โ”‚ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : 380%โ”‚ โ”‚ +โ”‚ โ”‚ โ—‹ ์„ ํƒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿช ์˜คํ”„๋ผ์ธ ๋ฐฉ์‹ โ”‚ โ”‚ +โ”‚ โ”‚ [์žฌ๋ฐฉ๋ฌธ ์ด๋ฒคํŠธโœ]โ”‚ โ”‚ +โ”‚ โ”‚ ๊ฒฝํ’ˆ: [์ƒํ’ˆ๊ถŒ1๋งŒโœ]โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ: ์˜์ˆ˜์ฆ์ œ์ถœ โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: 200๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ ๋น„์šฉ: 50๋งŒ์› โ”‚ โ”‚ +โ”‚ โ”‚ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : 320%โ”‚ โ”‚ +โ”‚ โ”‚ โ—‹ ์„ ํƒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ’Ž ์˜ต์…˜ 3: ๊ณ ๋น„์šฉโ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ ๐ŸŒ ์˜จ๋ผ์ธ ๋ฐฉ์‹ โ”‚ โ”‚ +โ”‚ โ”‚ [์œ ํŠœ๋ธŒ ๊ตฌ๋…..โœ]โ”‚ โ”‚ +โ”‚ โ”‚ ๊ฒฝํ’ˆ: [์ƒํ’ˆ๊ถŒ5๋งŒโœ]โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ: ์œ ํŠœ๋ธŒ๊ตฌ๋… โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: 400๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ ๋น„์šฉ: 95๋งŒ์› โ”‚ โ”‚ +โ”‚ โ”‚ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : 280%โ”‚ โ”‚ +โ”‚ โ”‚ โ—‹ ์„ ํƒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿช ์˜คํ”„๋ผ์ธ ๋ฐฉ์‹ โ”‚ โ”‚ +โ”‚ โ”‚ [VIP์ดˆ๋Œ€ ์ด๋ฒคํŠธโœ]โ”‚ โ”‚ +โ”‚ โ”‚ ๊ฒฝํ’ˆ: [๊ณ ๊ธ‰์ƒํ’ˆโœ]โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ: VIP ์ดˆ๋Œ€์žฅโ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: 300๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ ๋น„์šฉ: 100๋งŒ์› โ”‚ โ”‚ +โ”‚ โ”‚ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : 250%โ”‚ โ”‚ +โ”‚ โ”‚ โ—‹ ์„ ํƒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋‹ค์Œ ๋‹จ๊ณ„ โ”‚ โ”‚ โ† ์„ ํƒ ํ›„ ํ™œ์„ฑํ™” +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„โ”‚ โ† Nav Bottom +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ ํŽผ์นจ ์ƒํƒœ* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ–ผ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“Š ์—…์ข… ํŠธ๋ Œ๋“œ โ”‚ +โ”‚ โ€ข ์ตœ๊ทผ ์„ฑ๊ณต ์œ ํ˜•: โ”‚ +โ”‚ ํ• ์ธ ์ฟ ํฐ (์ฐธ์—ฌ์œจ โ”‚ +โ”‚ 35% โ†‘) โ”‚ +โ”‚ โ€ข ์„ ํ˜ธ ๊ฒฝํ’ˆ: โ”‚ +โ”‚ ์ปคํ”ผ/์ƒํ’ˆ๊ถŒ โ”‚ +โ”‚ โ€ข ํšจ๊ณผ์  ์ฐธ์—ฌ๋ฐฉ๋ฒ•: โ”‚ +โ”‚ ์ „ํ™”๋ฒˆํ˜ธ > SNS โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ ์ง€์—ญ ํŠน์„ฑ โ”‚ +โ”‚ โ€ข ์ง€์—ญ ์„ฑ๊ณต๋ฅ : โ”‚ +โ”‚ ์ˆ˜์› ์ง€์—ญ 28% โ†‘ โ”‚ +โ”‚ โ€ข ์ง€์—ญ ๊ณ ๊ฐ ํŠน์„ฑ: โ”‚ +โ”‚ 20-30๋Œ€ ์—ฌ์„ฑ ์ฃผ๋ ฅโ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ—“๏ธ ์‹œ์ฆŒ ํŠน์„ฑ โ”‚ +โ”‚ โ€ข ์ถ”์ฒœ ์ด๋ฒคํŠธ: โ”‚ +โ”‚ ๋ด„ ์‹ ๋ฉ”๋‰ด ํ”„๋กœ๋ชจ์…˜โ”‚ +โ”‚ โ€ข ํŠน๋ณ„ ์‹œ์ฆŒ: โ”‚ +โ”‚ ํ™”์ดํŠธ๋ฐ์ด (3/14) โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **๋กœ๋”ฉ**: + - AI ์•„์ด์ฝ˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ (ํšŒ์ „) + - ์ง„ํ–‰ ๋ฐ” 0% โ†’ 100% (10์ดˆ) + - ์™„๋ฃŒ ์‹œ ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜ +2. **ํŠธ๋ Œ๋“œ ๋ถ„์„ ์ ‘๊ธฐ/ํŽผ์น˜๊ธฐ**: ์•„์ฝ”๋””์–ธ ํ˜•ํƒœ +3. **์ œ๋ชฉ/๊ฒฝํ’ˆ ํŽธ์ง‘**: + - ์—ฐํ•„ ์•„์ด์ฝ˜ ํด๋ฆญ โ†’ ์ธ๋ผ์ธ ํŽธ์ง‘ ๋ชจ๋“œ + - ์ตœ๋Œ€ ๊ธ€์ž์ˆ˜ ํ‘œ์‹œ (์ œ๋ชฉ 50์ž, ๊ฒฝํ’ˆ 30์ž) + - Enter/์ €์žฅ ์‹œ ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ €์žฅ +4. **์˜ต์…˜ ์„ ํƒ**: ๋ผ๋””์˜ค ๋ฒ„ํŠผ, 1๊ฐœ๋งŒ ์„ ํƒ ๊ฐ€๋Šฅ +5. **๋‹ค์Œ ๋‹จ๊ณ„**: ์„ ํƒ ํ›„ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”, ์ฝ˜ํ…์ธ  ์ƒ์„ฑ ํ™”๋ฉด ์ด๋™ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์„ธ๋กœ ์Šคํฌ๋กค, ์นด๋“œ 1์—ด +- Tablet: ์นด๋“œ 1์—ด ์œ ์ง€, ๋„ˆ๋น„ ์ฆ๊ฐ€ +- Desktop: ์ขŒ์ธก ํŠธ๋ Œ๋“œ ๋ถ„์„ (30%) + ์šฐ์ธก ์ถ”์ฒœ์•ˆ (70%) + +--- + +#### 09-SNS์ด๋ฏธ์ง€์ƒ์„ฑ + +**๊ฐœ์š”** +- **๋ชฉ์ **: 3๊ฐ€์ง€ ์Šคํƒ€์ผ SNS ์ด๋ฏธ์ง€ ์ž๋™ ์ƒ์„ฑ +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-CONT-010 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: M/21 (ํ•„์ˆ˜, ์ค‘๊ฐ„ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ์ด๋ฒคํŠธ ์ •๋ณด ๊ธฐ๋ฐ˜ ์ด๋ฏธ์ง€ ์ƒ์„ฑ +- 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์ œ๊ณต (์‹ฌํ”Œ/ํ™”๋ ค/ํŠธ๋ Œ๋””) +- ์ƒ์„ฑ ์ค‘ ๋กœ๋”ฉ (5์ดˆ ์ด๋‚ด) +- ์ด๋ฏธ์ง€ ํ’€์Šคํฌ๋ฆฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px) - ์ƒ์„ฑ ์ค‘* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๐Ÿ‘คโ”‚ โ† Header ์ถ”๊ฐ€ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ [AI ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ค‘] โ”‚ +โ”‚ โ”‚ +โ”‚ ๋”ฅ๋Ÿฌ๋‹ ๋ชจ๋ธ์ด โ”‚ +โ”‚ ์ด๋ฒคํŠธ์— ์–ด์šธ๋ฆฌ๋Š” โ”‚ +โ”‚ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ณ  โ”‚ +โ”‚ ์žˆ์–ด์š”... โ”‚ +โ”‚ โ”‚ +โ”‚ [์Šคํ”ผ๋„ˆ ์• ๋‹ˆ๋ฉ”์ด์…˜] โ”‚ +โ”‚ โ”‚ +โ”‚ ์˜ˆ์ƒ ์‹œ๊ฐ„: 5์ดˆ โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„โ”‚ โ† Nav Bottom ์ถ”๊ฐ€ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Mobile (320-767px) - ์™„๋ฃŒ* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๐Ÿ‘คโ”‚ โ† Header +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ์Šคํƒ€์ผ 1: ์‹ฌํ”Œ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ [์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ]โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ [์ด๋ฒคํŠธ ์ œ๋ชฉ] โ”‚ โ”‚ +โ”‚ โ”‚ [๊ฒฝํ’ˆ ์ •๋ณด] โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ—‹ ์„ ํƒ | ๐Ÿ” ํฌ๊ฒŒ๋ณด๊ธฐ โ”‚ +โ”‚ โ”‚ +โ”‚ ์Šคํƒ€์ผ 2: ํ™”๋ ค โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [์ด๋ฏธ์ง€...] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ—‹ ์„ ํƒ | ๐Ÿ” ํฌ๊ฒŒ๋ณด๊ธฐ โ”‚ +โ”‚ โ”‚ +โ”‚ ์Šคํƒ€์ผ 3: ํŠธ๋ Œ๋”” โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [์ด๋ฏธ์ง€...] โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ—‹ ์„ ํƒ | ๐Ÿ” ํฌ๊ฒŒ๋ณด๊ธฐ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๊ฑด๋„ˆ๋›ฐ๊ธฐ | ๋‹ค์Œ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„โ”‚ โ† Nav Bottom +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **์ƒ์„ฑ ๋กœ๋”ฉ**: + - Skeleton Screen (์ด๋ฏธ์ง€ ์˜์—ญ) + - ์Šคํ”ผ๋„ˆ + ์ง„ํ–‰ ๋ฉ”์‹œ์ง€ +2. **์ด๋ฏธ์ง€ ์„ ํƒ**: ๋ผ๋””์˜ค ๋ฒ„ํŠผ, 1๊ฐœ ์„ ํƒ +3. **ํฌ๊ฒŒ๋ณด๊ธฐ**: ํ’€์Šคํฌ๋ฆฐ ๋ชจ๋‹ฌ, ํ•€์น˜ ์คŒ ๊ฐ€๋Šฅ +4. **๊ฑด๋„ˆ๋›ฐ๊ธฐ**: ์ด๋ฏธ์ง€ ์—†์ด ๋‹ค์Œ ๋‹จ๊ณ„ (์ฝ˜ํ…์ธ  ํŽธ์ง‘ ์Šคํ‚ต) +5. **๋‹ค์Œ**: ์„ ํƒํ•œ ์ด๋ฏธ์ง€๋กœ ์ฝ˜ํ…์ธ  ํŽธ์ง‘ ํ™”๋ฉด ์ด๋™ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์ด๋ฏธ์ง€ 1์—ด, ์„ธ๋กœ ์Šคํฌ๋กค +- Tablet: ์ด๋ฏธ์ง€ 2์—ด (1+2 ๋˜๋Š” 3์—ด) +- Desktop: ์ด๋ฏธ์ง€ 3์—ด ๊ฐ€๋กœ ๋ฐฐ์น˜ + +--- + +#### 10-์ฝ˜ํ…์ธ ํŽธ์ง‘ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ์ƒ์„ฑ๋œ SNS ์ด๋ฏธ์ง€ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-CONT-020 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: S/13 (๊ถŒ์žฅ, ์ค‘๊ฐ„ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ํ…์ŠคํŠธ ์ˆ˜์ • (์ œ๋ชฉ, ๊ฒฝํ’ˆ, ์ฐธ์—ฌ์•ˆ๋‚ด) +- ์ƒ‰์ƒ ์กฐ์ • (๋ฐฐ๊ฒฝ์ƒ‰, ํ…์ŠคํŠธ์ƒ‰, ๊ฐ•์กฐ์ƒ‰) +- ๋กœ๊ณ  ์œ„์น˜ ์กฐ์ • +- ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ์ฝ˜ํ…์ธ  ํŽธ์ง‘ ๐Ÿ‘คโ”‚ โ† Header +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๋ฏธ๋ฆฌ๋ณด๊ธฐ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ [์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ]โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ [์ˆ˜์ •๋œ ๋‚ด์šฉ โ”‚ โ”‚ +โ”‚ โ”‚ ์ฆ‰์‹œ ๋ฐ˜์˜] โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ ํ…์ŠคํŠธ ํŽธ์ง‘ โ”‚ +โ”‚ ์ œ๋ชฉ: โ”‚ +โ”‚ [์‹ ๊ทœ๊ณ ๊ฐ ์ด๋ฒคํŠธ...] โ”‚ +โ”‚ โ”‚ +โ”‚ ๊ฒฝํ’ˆ: โ”‚ +โ”‚ [์ปคํ”ผ ์ฟ ํฐ 100๋งค...]โ”‚ +โ”‚ โ”‚ +โ”‚ ์ฐธ์—ฌ์•ˆ๋‚ด: โ”‚ +โ”‚ [์ „ํ™”๋ฒˆํ˜ธ ์ž…๋ ฅ...] โ”‚ +โ”‚ โ”‚ +โ”‚ ๐ŸŽจ ์ƒ‰์ƒ ์กฐ์ • โ”‚ +โ”‚ ๋ฐฐ๊ฒฝ์ƒ‰: [โ–  ์„ ํƒ] โ”‚ +โ”‚ ํ…์ŠคํŠธ: [โ–  ์„ ํƒ] โ”‚ +โ”‚ ๊ฐ•์กฐ์ƒ‰: [โ–  ์„ ํƒ] โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿข ๋กœ๊ณ  ์œ„์น˜ โ”‚ +โ”‚ [โ—€ ์œ„์น˜์ด๋™ โ–ถ] โ”‚ +โ”‚ [- ํฌ๊ธฐ์กฐ์ ˆ +] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ €์žฅ | ๋‹ค์Œ ๋‹จ๊ณ„ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„ โ”‚ โ† Nav Bottom +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Desktop (1024px+)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ† ์ฝ˜ํ…์ธ  ํŽธ์ง‘ [ํ”„๋กœํ•„]โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์‚ฌ์ด๋“œ๋ฐ”] โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ”‚๋ฏธ๋ฆฌ๋ณด๊ธฐ โ”‚ โ”‚ํŽธ์ง‘ ํŒจ๋„ โ”‚โ”‚ +โ”‚ โ€ข ๋Œ€์‹œ๋ณด๋“œ โ”‚ โ”‚ โ”‚ โ”‚โ”‚ +โ”‚ โ€ข ์ด๋ฒคํŠธ โ”‚ โ”‚ โ”‚๐Ÿ“ ํ…์ŠคํŠธ โ”‚โ”‚ +โ”‚ โ€ข ๋ถ„์„ โ”‚[์‹ค์‹œ๊ฐ„ โ”‚ โ”‚์ œ๋ชฉ: โ”‚โ”‚ +โ”‚ โ€ข ํ”„๋กœํ•„ โ”‚ ๋ฏธ๋ฆฌ๋ณด๊ธฐ]โ”‚ โ”‚[์ž…๋ ฅ์ฐฝ] โ”‚โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚๊ฒฝํ’ˆ: โ”‚โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚[์ž…๋ ฅ์ฐฝ] โ”‚โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚๐ŸŽจ ์ƒ‰์ƒ โ”‚โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚๋ฐฐ๊ฒฝ: [โ– ] โ”‚โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ํ…์ŠคํŠธ:[โ– ] โ”‚โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚๊ฐ•์กฐ: [โ– ] โ”‚โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚๐Ÿข ๋กœ๊ณ  โ”‚โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚์œ„์น˜/ํฌ๊ธฐ โ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ €์žฅ โ”‚ โ”‚๋‹ค์Œ ๋‹จ๊ณ„ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **ํ…์ŠคํŠธ ํŽธ์ง‘**: ์ž…๋ ฅ ์‹œ ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์—…๋ฐ์ดํŠธ +2. **์ƒ‰์ƒ ์„ ํƒ**: ์ปฌ๋Ÿฌ ํ”ผ์ปค ํŒ์—…, ์„ ํƒ ์‹œ ์ฆ‰์‹œ ๋ฐ˜์˜ +3. **๋กœ๊ณ  ์œ„์น˜**: ๋“œ๋ž˜๊ทธ ์•ค ๋“œ๋กญ ๋˜๋Š” ํ™”์‚ดํ‘œ ๋ฒ„ํŠผ +4. **์ €์žฅ**: ํŽธ์ง‘ ๋‚ด์šฉ ์ €์žฅ, ํ˜„์žฌ ํ™”๋ฉด ์œ ์ง€ +5. **๋‹ค์Œ ๋‹จ๊ณ„**: ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์„ธ๋กœ ๋ฐฐ์น˜ (๋ฏธ๋ฆฌ๋ณด๊ธฐ โ†’ ํŽธ์ง‘ ์˜ต์…˜) +- Tablet: ์„ธ๋กœ ๋ฐฐ์น˜ ์œ ์ง€, ์ž…๋ ฅ ์˜์—ญ ํ™•๋Œ€ +- Desktop: ์ขŒ์šฐ ๋ถ„ํ•  (๋ฏธ๋ฆฌ๋ณด๊ธฐ 60% + ํŽธ์ง‘ ํŒจ๋„ 40%) + +--- + +#### 11-๋ฐฐํฌ์ฑ„๋„์„ ํƒ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ๋‹ค์ค‘ ์ฑ„๋„ ์„ ํƒ ๋ฐ ๋ฐฐํฌ ์„ค์ • +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-EVENT-040 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: M/13 (ํ•„์ˆ˜, ์ค‘๊ฐ„ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- 4๊ฐ€์ง€ ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ (์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS) +- ์ฑ„๋„๋ณ„ ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜ ํ‘œ์‹œ +- ์ฑ„๋„๋ณ„ ๋น„์šฉ ์•ˆ๋‚ด +- ์ด ์˜ˆ์ƒ ๋น„์šฉ ํ•ฉ๊ณ„ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ ๐Ÿ‘คโ”‚ โ† Header +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ๋ฐฐํฌ ์ฑ„๋„์„ ์„ ํƒํ•ด โ”‚ +โ”‚ ์ฃผ์„ธ์š” (์ตœ์†Œ 1๊ฐœ) โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ˜‘ ์šฐ๋ฆฌ๋™๋„คTV โ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ ๋ฐ˜๊ฒฝ: [500m โ–ผ] โ”‚ โ”‚ +โ”‚ โ”‚ ์‹œ๊ฐ„: [์ €๋… โ–ผ] โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: 5๋งŒ๋ช… ๋…ธ์ถœ โ”‚ โ”‚ +โ”‚ โ”‚ ๋น„์šฉ: 8๋งŒ์› โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ˜‘ ๋ง๊ณ ๋น„์ฆˆ โ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ ๋งค์žฅ ์ „ํ™”๋ฒˆํ˜ธ: โ”‚ โ”‚ +โ”‚ โ”‚ 010-1234-5678 โ”‚ โ”‚ +โ”‚ โ”‚ ์—ฐ๊ฒฐ์Œ ์—…๋ฐ์ดํŠธ โ”‚ โ”‚ +โ”‚ โ”‚ ๋น„์šฉ: ๋ฌด๋ฃŒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ˜ ์ง€๋‹ˆTV ๊ด‘๊ณ  โ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ ์ง€์—ญ: [์ˆ˜์› โ–ผ] โ”‚ โ”‚ +โ”‚ โ”‚ ์‹œ๊ฐ„: [์ „์ฒด โ–ผ] โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์‚ฐ: [์ž…๋ ฅ...] โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์ƒ: ๊ณ„์‚ฐ์ค‘... โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ˜‘ SNS โ”‚ โ”‚ +โ”‚ โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚ โ˜‘ Instagram โ”‚ โ”‚ +โ”‚ โ”‚ โ˜‘ Naver Blog โ”‚ โ”‚ +โ”‚ โ”‚ โ˜ Kakao Channel โ”‚ โ”‚ +โ”‚ โ”‚ ์˜ˆ์•ฝ: [์ฆ‰์‹œ โ–ผ] โ”‚ โ”‚ +โ”‚ โ”‚ ๋น„์šฉ: ๋ฌด๋ฃŒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ด ์˜ˆ์ƒ ๋น„์šฉ: โ”‚ โ”‚ +โ”‚ โ”‚ 8๋งŒ์› โ”‚ โ”‚ +โ”‚ โ”‚ ์ด ์˜ˆ์ƒ ๋…ธ์ถœ: โ”‚ โ”‚ +โ”‚ โ”‚ 5๋งŒ๋ช…+ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋‹ค์Œ ๋‹จ๊ณ„ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„ โ”‚ โ† Nav Bottom +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Desktop (1024px+)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ† ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ [ํ”„๋กœํ•„]โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์‚ฌ์ด๋“œ๋ฐ”] โ”‚ +โ”‚ ๋ฐฐํฌ ์ฑ„๋„์„ ์„ ํƒํ•ด์ฃผ์„ธ์š” โ”‚ +โ”‚ โ€ข ๋Œ€์‹œ๋ณด๋“œ (์ตœ์†Œ 1๊ฐœ ์ด์ƒ) โ”‚ +โ”‚ โ€ข ์ด๋ฒคํŠธ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ +โ”‚ โ€ข ๋ถ„์„ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ€ข ํ”„๋กœํ•„ โ”‚โ˜‘์šฐ๋ฆฌ๋™๋„คโ”‚โ”‚โ˜‘๋ง๊ณ ๋น„์ฆˆโ”‚ โ”‚ +โ”‚ โ”‚ TV โ”‚โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚๋ฐ˜๊ฒฝ: โ”‚โ”‚์ „ํ™”๋ฒˆํ˜ธโ”‚โ”‚ โ”‚ +โ”‚ โ”‚[500mโ–ผ]โ”‚โ”‚์ž๋™์—ฐ๋™โ”‚โ”‚ โ”‚ +โ”‚ โ”‚์‹œ๊ฐ„: โ”‚โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚[์ €๋…โ–ผ]โ”‚โ”‚์—ฐ๊ฒฐ์Œ โ”‚โ”‚ โ”‚ +โ”‚ โ”‚5๋งŒ๋ช… โ”‚โ”‚์—…๋ฐ์ดํŠธโ”‚โ”‚ โ”‚ +โ”‚ โ”‚8๋งŒ์› โ”‚โ”‚๋ฌด๋ฃŒ โ”‚โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚โ˜์ง€๋‹ˆTVโ”‚โ”‚โ˜‘ SNS โ”‚ โ”‚ +โ”‚ โ”‚ ๊ด‘๊ณ  โ”‚โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ +โ”‚ โ”‚์ง€์—ญ: โ”‚โ”‚โ˜‘Insta โ”‚ โ”‚ +โ”‚ โ”‚[์ˆ˜์›โ–ผ]โ”‚โ”‚โ˜‘Naver โ”‚ โ”‚ +โ”‚ โ”‚์‹œ๊ฐ„: โ”‚โ”‚โ˜Kakao โ”‚ โ”‚ +โ”‚ โ”‚[์ „์ฒดโ–ผ]โ”‚โ”‚์˜ˆ์•ฝ:์ฆ‰์‹œโ”‚ โ”‚ +โ”‚ โ”‚์˜ˆ์‚ฐ: โ”‚โ”‚๋ฌด๋ฃŒ โ”‚ โ”‚ +โ”‚ โ”‚[์ž…๋ ฅ...]โ”‚โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚์ด ์˜ˆ์ƒ ๋น„์šฉ: 8๋งŒ์›โ”‚ โ”‚ +โ”‚ โ”‚์ด ์˜ˆ์ƒ ๋…ธ์ถœ: 5๋งŒ๋ช…+โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚๋‹ค์Œ ๋‹จ๊ณ„ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **์ฑ„๋„ ์„ ํƒ**: ์ฒดํฌ๋ฐ•์Šค, ๋‹ค์ค‘ ์„ ํƒ ๊ฐ€๋Šฅ +2. **์˜ต์…˜ ์„ค์ •**: ๊ฐ ์ฑ„๋„๋ณ„ ์„ธ๋ถ€ ์˜ต์…˜ ์ž…๋ ฅ +3. **์‹ค์‹œ๊ฐ„ ๊ณ„์‚ฐ**: ์„ ํƒ/์˜ต์…˜ ๋ณ€๊ฒฝ ์‹œ ๋น„์šฉ/๋…ธ์ถœ ์ˆ˜ ์ž๋™ ์—…๋ฐ์ดํŠธ +4. **๋‹ค์Œ ๋‹จ๊ณ„**: ์ตœ์†Œ 1๊ฐœ ์„ ํƒ ์‹œ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”, ์ตœ์ข… ์Šน์ธ ํ™”๋ฉด ์ด๋™ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์นด๋“œ 1์—ด ์„ธ๋กœ ๋ฐฐ์น˜ +- Tablet: ์นด๋“œ 2์—ด ๋ฐฐ์น˜ +- Desktop: ์นด๋“œ 2ร—2 ๊ทธ๋ฆฌ๋“œ ๋ฐฐ์น˜ + +--- + +#### 12-์ตœ์ข…์Šน์ธ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ๋ชจ๋“  ์„ค์ • ์ตœ์ข… ํ™•์ธ ๋ฐ ์Šน์ธ +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-EVENT-050 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: M/13 (ํ•„์ˆ˜, ์ค‘๊ฐ„ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ์ด๋ฒคํŠธ ์ •๋ณด ์š”์•ฝ (์ œ๋ชฉ, ๊ฒฝํ’ˆ, ๊ธฐ๊ฐ„) +- SNS ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ +- ๋ฐฐํฌ ์ฑ„๋„ ๋ชฉ๋ก +- ์ˆ˜์ • ๋ฒ„ํŠผ (๊ฐ ์„น์…˜๋ณ„ ์ด์ „ ๋‹จ๊ณ„ ์ด๋™) +- ์Šน์ธ ๋ฐ ๋ฐฐํฌ ๋ฒ„ํŠผ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ์ตœ์ข… ์Šน์ธ ๐Ÿ‘คโ”‚ โ† Header ์ถ”๊ฐ€ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โœ“ ์ด๋ฒคํŠธ ์ •๋ณด [์ˆ˜์ •]โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ +โ”‚ โ€ข ์ œ๋ชฉ: ์‹ ๊ทœ๊ณ ๊ฐ... โ”‚ +โ”‚ โ€ข ๊ฒฝํ’ˆ: ์ปคํ”ผ ์ฟ ํฐ โ”‚ +โ”‚ โ€ข ๊ธฐ๊ฐ„: 2025-11-01 ~ โ”‚ +โ”‚ 2025-11-15 โ”‚ +โ”‚ โ€ข ์ฐธ์—ฌ๋ฐฉ๋ฒ•: ์ „ํ™”๋ฒˆํ˜ธ โ”‚ +โ”‚ โ”‚ +โ”‚ โœ“ SNS ์ด๋ฏธ์ง€ [์ˆ˜์ •]โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [์„ ํƒํ•œ ์ด๋ฏธ์ง€] โ”‚ โ”‚ +โ”‚ โ”‚ (์Šคํƒ€์ผ 1: ์‹ฌํ”Œ) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โœ“ ๋ฐฐํฌ ์ฑ„๋„ [์ˆ˜์ •]โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ +โ”‚ โ€ข ์šฐ๋ฆฌ๋™๋„คTV (์˜ˆ์ƒ 5๋งŒ๋ช…)โ”‚ +โ”‚ โ€ข ๋ง๊ณ ๋น„์ฆˆ (์˜ˆ์ƒ 3๋งŒ๋ช…) โ”‚ +โ”‚ โ€ข SNS (๋„ค์ด๋ฒ„, ์นด์นด์˜ค) โ”‚ +โ”‚ โ”‚ +โ”‚ โœ“ ์˜ˆ์ƒ ์„ฑ๊ณผ โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ +โ”‚ โ€ข ์ด ์ฐธ์—ฌ์ž: 150๋ช… โ”‚ +โ”‚ โ€ข ์ด ๋น„์šฉ: 30๋งŒ์› โ”‚ +โ”‚ โ€ข ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : 450%โ”‚ +โ”‚ โ”‚ +โ”‚ โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ โ”‚ +โ”‚ ๋ฐฐํฌ ํ›„์—๋Š” ์ด๋ฒคํŠธ ์ œ๋ชฉ,โ”‚ +โ”‚ ๊ฒฝํ’ˆ, ๊ธฐ๊ฐ„์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ โ”‚ +โ”‚ ์—†์Šต๋‹ˆ๋‹ค. โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์Šน์ธ ๋ฐ ๋ฐฐํฌ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„โ”‚ โ† Nav Bottom ์ถ”๊ฐ€ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **์ˆ˜์ • ๋ฒ„ํŠผ**: ๊ฐ ์„น์…˜๋ณ„ ์ด์ „ ๋‹จ๊ณ„ ํ™”๋ฉด ์ด๋™ +2. **์Šน์ธ ํ™•์ธ**: "์ด๋ฒคํŠธ๋ฅผ ์Šน์ธํ•˜๊ณ  ๋ฐฐํฌํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" ๋‹ค์ด์–ผ๋กœ๊ทธ +3. **์Šน์ธ ์™„๋ฃŒ**: + - ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ (๋ฐฐํฌ ์ค‘...) + - ์„ฑ๊ณต ํ† ์ŠคํŠธ: "์ด๋ฒคํŠธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋ฐฐํฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + - ๋Œ€์‹œ๋ณด๋“œ ํ™”๋ฉด ์ด๋™ + ์ƒˆ ์ด๋ฒคํŠธ ์นด๋“œ ์ถ”๊ฐ€ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์ „์ฒด ํ™”๋ฉด, ์„ธ๋กœ ์Šคํฌ๋กค +- Tablet/Desktop: ์ค‘์•™ ์นด๋“œ (์ตœ๋Œ€ ๋„ˆ๋น„ 800px), ์ขŒ์šฐ ์—ฌ๋ฐฑ + +--- + +#### 13-์ด๋ฒคํŠธ์ƒ์„ธ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ์ง„ํ–‰ ์ค‘์ธ ์ด๋ฒคํŠธ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-EVENT-060 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: S/13 (๊ถŒ์žฅ, ์ค‘๊ฐ„ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ์ด๋ฒคํŠธ ๊ธฐ๋ณธ ์ •๋ณด ํ‘œ์‹œ +- ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ (์ฐธ์—ฌ์ž, ๋…ธ์ถœ์ˆ˜, ์กฐํšŒ์ˆ˜, ๊ณต์œ ์ˆ˜) +- ๋ฐฐํฌ ์ฑ„๋„ ํ˜„ํ™ฉ +- ์ตœ๊ทผ ์ฐธ์—ฌ์ž 10๋ช… +- ์ด๋ฒคํŠธ ๊ด€๋ฆฌ ์•ก์…˜ (์ˆ˜์ •, ์ข…๋ฃŒ) + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ์ด๋ฒคํŠธ ์ƒ์„ธ ๐Ÿ‘คโ”‚ โ† Header +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ์‹ ๊ทœ๊ณ ๊ฐ ์œ ์น˜ ์ด๋ฒคํŠธ โ”‚ +โ”‚ ์ง„ํ–‰์ค‘ | D-5 โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“Š ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ”‚์ฐธ์—ฌ์ž์ˆ˜ โ”‚โ”‚๋…ธ์ถœ์ˆ˜ โ”‚โ”‚ +โ”‚ โ”‚ 128๋ช… โ”‚โ”‚5.2๋งŒํšŒ โ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ”‚์กฐํšŒ์ˆ˜ โ”‚โ”‚๊ณต์œ ์ˆ˜ โ”‚โ”‚ +โ”‚ โ”‚ 3.8๋งŒํšŒโ”‚โ”‚ 156ํšŒ โ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“‹ ๊ธฐ๋ณธ ์ •๋ณด โ”‚ +โ”‚ ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„: โ”‚ +โ”‚ 2025-11-01~11-15 โ”‚ +โ”‚ โ”‚ +โ”‚ ๊ฒฝํ’ˆ: โ”‚ +โ”‚ ์ปคํ”ผ ์ฟ ํฐ 100๋งค โ”‚ +โ”‚ โ”‚ +โ”‚ ์ฐธ์—ฌ ๋ฐฉ๋ฒ•: โ”‚ +โ”‚ ์ „ํ™”๋ฒˆํ˜ธ ์ž…๋ ฅ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“บ ๋ฐฐํฌ ์ฑ„๋„ ํ˜„ํ™ฉ โ”‚ +โ”‚ โœ… ์šฐ๋ฆฌ๋™๋„คTV โ”‚ +โ”‚ ๋…ธ์ถœ: 4.8๋งŒํšŒ โ”‚ +โ”‚ โœ… ๋ง๊ณ ๋น„์ฆˆ โ”‚ +โ”‚ ์—ฐ๊ฒฐ์Œ ์—…๋ฐ์ดํŠธ๋จ โ”‚ +โ”‚ โœ… SNS โ”‚ +โ”‚ Instagram: 220ํšŒ โ”‚ +โ”‚ Naver: 180ํšŒ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ‘ฅ ์ตœ๊ทผ ์ฐธ์—ฌ์ž โ”‚ +โ”‚ โ€ข ๊น€** (010-****-1234)โ”‚ +โ”‚ โ€ข ์ด** (010-****-5678)โ”‚ +โ”‚ โ€ข ๋ฐ•** (010-****-9012)โ”‚ +โ”‚ ... โ”‚ +โ”‚ [์ „์ฒด ์ฐธ์—ฌ์ž ๋ณด๊ธฐ >] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ํšจ๊ณผ์ธก์ • ๋Œ€์‹œ๋ณด๋“œโ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ด๋ฒคํŠธ ์ˆ˜์ • โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ด๋ฒคํŠธ ์ข…๋ฃŒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„ โ”‚ โ† Nav Bottom +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Desktop (1024px+)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ† ์ด๋ฒคํŠธ ์ƒ์„ธ [ํ”„๋กœํ•„]โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์‚ฌ์ด๋“œ๋ฐ”] ์‹ ๊ทœ๊ณ ๊ฐ ์œ ์น˜ ์ด๋ฒคํŠธ โ”‚ +โ”‚ ์ง„ํ–‰์ค‘ | D-5 โ”‚ +โ”‚ โ€ข ๋Œ€์‹œ๋ณด๋“œ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ +โ”‚ โ€ข ์ด๋ฒคํŠธ ๐Ÿ“Š ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ โ”‚ +โ”‚ โ€ข ๋ถ„์„ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ€ข ํ”„๋กœํ•„ โ”‚์ฐธ์—ฌ์ž โ”‚โ”‚๋…ธ์ถœ์ˆ˜ โ”‚โ”‚์กฐํšŒ์ˆ˜ โ”‚โ”‚ +โ”‚ โ”‚128๋ช… โ”‚โ”‚5.2๋งŒํšŒโ”‚โ”‚3.8๋งŒํšŒโ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚๊ณต์œ ์ˆ˜ โ”‚ โ”‚ +โ”‚ โ”‚156ํšŒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ”‚๐Ÿ“‹ ๊ธฐ๋ณธ ์ •๋ณด โ”‚โ”‚๐Ÿ“บ ๋ฐฐํฌํ˜„ํ™ฉโ”‚โ”‚ +โ”‚ โ”‚ โ”‚โ”‚ โ”‚โ”‚ +โ”‚ โ”‚๊ธฐ๊ฐ„: โ”‚โ”‚โœ…์šฐ๋ฆฌ๋™๋„คTVโ”‚โ”‚ +โ”‚ โ”‚11/1~11/15 โ”‚โ”‚ 4.8๋งŒํšŒ โ”‚โ”‚ +โ”‚ โ”‚ โ”‚โ”‚ โ”‚โ”‚ +โ”‚ โ”‚๊ฒฝํ’ˆ: โ”‚โ”‚โœ…๋ง๊ณ ๋น„์ฆˆโ”‚โ”‚ +โ”‚ โ”‚์ปคํ”ผ์ฟ ํฐ100 โ”‚โ”‚ ์—…๋ฐ์ดํŠธโ”‚โ”‚ +โ”‚ โ”‚ โ”‚โ”‚ โ”‚โ”‚ +โ”‚ โ”‚์ฐธ์—ฌ: โ”‚โ”‚โœ…SNS โ”‚โ”‚ +โ”‚ โ”‚์ „ํ™”๋ฒˆํ˜ธ โ”‚โ”‚ Insta:220โ”‚โ”‚ +โ”‚ โ”‚ โ”‚โ”‚ Naver:180โ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ‘ฅ ์ตœ๊ทผ ์ฐธ์—ฌ์ž [์ „์ฒด๋ณด๊ธฐ >] โ”‚ +โ”‚ ๊น€** (010-****-1234) โ”‚ +โ”‚ ์ด** (010-****-5678) โ”‚ +โ”‚ ๋ฐ•** (010-****-9012) โ”‚ +โ”‚ ... โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ํšจ๊ณผ์ธก์ •โ”‚โ”‚์ˆ˜์ • โ”‚โ”‚์ข…๋ฃŒ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„**: 5๋ถ„ ๊ฐ„๊ฒฉ ์ž๋™ ๊ฐฑ์‹  +2. **์ „์ฒด ์ฐธ์—ฌ์ž ๋ณด๊ธฐ**: ์ฐธ์—ฌ์ž ๋ชฉ๋ก ํ™”๋ฉด์œผ๋กœ ์ด๋™ +3. **ํšจ๊ณผ์ธก์ • ๋Œ€์‹œ๋ณด๋“œ**: ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ํ™”๋ฉด์œผ๋กœ ์ด๋™ +4. **์ด๋ฒคํŠธ ์ˆ˜์ •**: ์ œํ•œ์  ์ˆ˜์ • ๊ฐ€๋Šฅ (๋ฐฐํฌ ํ›„ ์ œ๋ชฉ/๊ฒฝํ’ˆ ์ˆ˜์ • ๋ถˆ๊ฐ€) +5. **์ด๋ฒคํŠธ ์ข…๋ฃŒ**: ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ›„ ์ฆ‰์‹œ ์ข…๋ฃŒ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์„น์…˜๋ณ„ ์„ธ๋กœ ๋ฐฐ์น˜ +- Tablet: 2์—ด ๋ฐฐ์น˜ (๊ธฐ๋ณธ์ •๋ณด + ๋ฐฐํฌํ˜„ํ™ฉ) +- Desktop: ๊ทธ๋ฆฌ๋“œ ๋ฐฐ์น˜ (ํ†ต๊ณ„ ์ƒ๋‹จ, ์ •๋ณด 2์—ด, ์ฐธ์—ฌ์ž ํ•˜๋‹จ) + +--- + +#### 14-์ฐธ์—ฌ์ž๋ชฉ๋ก + +**๊ฐœ์š”** +- **๋ชฉ์ **: ์ด๋ฒคํŠธ ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ ๋ฐ ๊ด€๋ฆฌ +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-PART-020 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: S/8 (๊ถŒ์žฅ, ๋‚ฎ์€ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ์ฐธ์—ฌ์ž ํ…Œ์ด๋ธ” (์‘๋ชจ๋ฒˆํ˜ธ, ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ฐธ์—ฌ๊ฒฝ๋กœ, ์ฐธ์—ฌ์ผ์‹œ, ๋‹น์ฒจ์—ฌ๋ถ€) +- ์ฐธ์—ฌ ๊ฒฝ๋กœ๋ณ„ ํ•„ํ„ฐ +- ๋‹น์ฒจ ์—ฌ๋ถ€ ํ•„ํ„ฐ +- ์ด๋ฆ„/์ „ํ™”๋ฒˆํ˜ธ ๊ฒ€์ƒ‰ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ์ฐธ์—ฌ์ž ๋ชฉ๋ก ๐Ÿ‘คโ”‚ โ† Header +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [๊ฒ€์ƒ‰์ฐฝ] โ”‚ +โ”‚ ๐Ÿ” ์ด๋ฆ„/์ „ํ™”๋ฒˆํ˜ธ... โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“‚ ํ•„ํ„ฐ โ”‚ +โ”‚ [์ „์ฒด๊ฒฝ๋กœโ–ผ] [์ „์ฒดโ–ผ] โ”‚ +โ”‚ โ”‚ +โ”‚ ์ด 128๋ช… ์ฐธ์—ฌ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ #0001 โ”‚ โ”‚ +โ”‚ โ”‚ ๊น€** โ”‚ โ”‚ +โ”‚ โ”‚ 010-****-1234 โ”‚ โ”‚ +โ”‚ โ”‚ SNS (Instagram) โ”‚ โ”‚ +โ”‚ โ”‚ 2025-11-02 14:23โ”‚ โ”‚ +โ”‚ โ”‚ ๋‹น์ฒจ ๋Œ€๊ธฐ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ #0002 โ”‚ โ”‚ +โ”‚ โ”‚ ์ด** โ”‚ โ”‚ +โ”‚ โ”‚ 010-****-5678 โ”‚ โ”‚ +โ”‚ โ”‚ ์šฐ๋ฆฌ๋™๋„คTV โ”‚ โ”‚ +โ”‚ โ”‚ 2025-11-02 15:45โ”‚ โ”‚ +โ”‚ โ”‚ ๋‹น์ฒจ ๋Œ€๊ธฐ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ #0003 โ”‚ โ”‚ +โ”‚ โ”‚ ๋ฐ•** โ”‚ โ”‚ +โ”‚ โ”‚ 010-****-9012 โ”‚ โ”‚ +โ”‚ โ”‚ ๋ง๊ณ ๋น„์ฆˆ โ”‚ โ”‚ +โ”‚ โ”‚ 2025-11-02 16:12โ”‚ โ”‚ +โ”‚ โ”‚ ๋‹น์ฒจ ๋Œ€๊ธฐ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ [๋”๋ณด๊ธฐ...] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„ โ”‚ โ† Nav Bottom +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Desktop (1024px+)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ† ์ฐธ์—ฌ์ž ๋ชฉ๋ก [ํ”„๋กœํ•„]โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ [์‚ฌ์ด๋“œ๋ฐ”] ๐Ÿ” [๊ฒ€์ƒ‰์ฐฝ] ๐Ÿ“‚ ํ•„ํ„ฐ โ”‚ +โ”‚ ์ด๋ฆ„/์ „ํ™”๋ฒˆํ˜ธ... [์ „์ฒด๊ฒฝ๋กœโ–ผ]โ”‚ +โ”‚ โ€ข ๋Œ€์‹œ๋ณด๋“œ [์ „์ฒดโ–ผ] โ”‚ +โ”‚ โ€ข ์ด๋ฒคํŠธ ์ด 128๋ช… ์ฐธ์—ฌ โ”‚ +โ”‚ โ€ข ๋ถ„์„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ +โ”‚ โ€ข ํ”„๋กœํ•„ โ”‚๋ฒˆํ˜ธโ”‚์ด๋ฆ„โ”‚์ „ํ™”๋ฒˆํ˜ธโ”‚๊ฒฝ๋กœโ”‚์ผ์‹œโ”‚๋‹น์ฒจโ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”ค +โ”‚ โ”‚0001โ”‚๊น€**โ”‚010-****โ”‚SNSโ”‚11/2โ”‚๋Œ€๊ธฐโ”‚ +โ”‚ โ”‚ โ”‚ โ”‚-1234 โ”‚ โ”‚14:23โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”ค +โ”‚ โ”‚0002โ”‚์ด**โ”‚010-****โ”‚์šฐ๋ฆฌโ”‚11/2โ”‚๋Œ€๊ธฐโ”‚ +โ”‚ โ”‚ โ”‚ โ”‚-5678 โ”‚๋™๋„คโ”‚15:45โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”ค +โ”‚ โ”‚0003โ”‚๋ฐ•**โ”‚010-****โ”‚๋ง๊ณ โ”‚11/2โ”‚๋Œ€๊ธฐโ”‚ +โ”‚ โ”‚ โ”‚ โ”‚-9012 โ”‚๋น„์ฆˆโ”‚16:12โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”ผโ”€โ”€โ”ค +โ”‚ โ”‚... โ”‚...โ”‚... โ”‚... โ”‚... โ”‚...โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”ดโ”€โ”€โ”˜ +โ”‚ โ”‚ +โ”‚ [1] 2 3 4 5 ... 7 > โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚์—‘์…€ ๋‹ค์šด๋กœ๋“œ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **๊ฒ€์ƒ‰**: ์ž…๋ ฅ ์ค‘ ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—…๋ฐ์ดํŠธ +2. **ํ•„ํ„ฐ ์ ์šฉ**: ๊ฒฝ๋กœ/๋‹น์ฒจ์—ฌ๋ถ€ ์„ ํƒ ์‹œ ์ฆ‰์‹œ ๋ชฉ๋ก ๊ฐฑ์‹  +3. **์นด๋“œ/ํ–‰ ํด๋ฆญ**: ์ฐธ์—ฌ์ž ์ƒ์„ธ ์ •๋ณด ๋ชจ๋‹ฌ ํ‘œ์‹œ +4. **์—‘์…€ ๋‹ค์šด๋กœ๋“œ**: ํ˜„์žฌ ํ•„ํ„ฐ ๊ธฐ์ค€ ์ฐธ์—ฌ์ž ๋ชฉ๋ก ๋‹ค์šด๋กœ๋“œ +5. **ํŽ˜์ด์ง€๋„ค์ด์…˜**: ํŽ˜์ด์ง€๋‹น 20๊ฐœ ํ‘œ์‹œ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์นด๋“œ ํ˜•ํƒœ 1์—ด ํ‘œ์‹œ +- Tablet: ์นด๋“œ ํ˜•ํƒœ 2์—ด ํ‘œ์‹œ +- Desktop: ํ…Œ์ด๋ธ” ํ˜•ํƒœ ํ‘œ์‹œ + +--- + +### 5.4 ์ฐธ์—ฌ์ž ๊ด€๋ฆฌ + +#### 15-์ด๋ฒคํŠธ์ฐธ์—ฌ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ๊ณ ๊ฐ์ด ์ด๋ฒคํŠธ์— ์ฐธ์—ฌํ•˜๋Š” ํ™”๋ฉด (๋ชจ๋ฐ”์ผ ์ตœ์ ํ™” ์ตœ์šฐ์„ ) +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-PART-010 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: M/13 (ํ•„์ˆ˜, ์ค‘๊ฐ„ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ์ด๋ฒคํŠธ ์ •๋ณด ํ‘œ์‹œ (์ œ๋ชฉ, ๊ฒฝํ’ˆ, ๊ธฐ๊ฐ„) +- SNS ์ด๋ฏธ์ง€ ํ‘œ์‹œ +- ์ฐธ์—ฌ ํผ (์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ) +- ๊ฐœ์ธ์ •๋ณด ๋™์˜ ์ฒดํฌ๋ฐ•์Šค +- ์ฐธ์—ฌ ์™„๋ฃŒ ์• ๋‹ˆ๋ฉ”์ด์…˜ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ [์ด๋ฒคํŠธ ์ด๋ฏธ์ง€] โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐ŸŽ‰ ์‹ ๊ทœ๊ณ ๊ฐ ์œ ์น˜ ์ด๋ฒคํŠธโ”‚ +โ”‚ โ”‚ +โ”‚ ๊ฒฝํ’ˆ: โ˜• ์ปคํ”ผ ์ฟ ํฐ โ”‚ +โ”‚ ๊ธฐ๊ฐ„: 2025-11-01 ~ โ”‚ +โ”‚ 2025-11-15 โ”‚ +โ”‚ โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ +โ”‚ โ”‚ +โ”‚ ์ฐธ์—ฌํ•˜๊ธฐ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ด๋ฆ„ * โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ „ํ™”๋ฒˆํ˜ธ * โ”‚ โ”‚ +โ”‚ โ”‚ 010-0000-0000 โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ˜ ๊ฐœ์ธ์ •๋ณด ์ˆ˜์ง‘ ๋ฐ โ”‚ +โ”‚ ์ด์šฉ์— ๋™์˜ํ•ฉ๋‹ˆ๋‹ค โ”‚ +โ”‚ [์ „๋ฌธ๋ณด๊ธฐ] โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌํ•˜๊ธฐ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ์ฐธ์—ฌ์ž: 128๋ช… โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*์ฐธ์—ฌ ์™„๋ฃŒ ์ƒํƒœ* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โœ“ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ์ฐธ์—ฌ๊ฐ€ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! โ”‚ +โ”‚ โ”‚ +โ”‚ ํ™๊ธธ๋™๋‹˜์˜ ํ–‰์šด์„ โ”‚ +โ”‚ ๊ธฐ์›ํ•ฉ๋‹ˆ๋‹ค! โ”‚ +โ”‚ โ”‚ +โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ +โ”‚ โ”‚ +โ”‚ ๋‹น์ฒจ์ž ๋ฐœํ‘œ โ”‚ +โ”‚ 2025-11-16 (์›”) โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ”‚ ํ™•์ธ โ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **ํผ ๊ฒ€์ฆ**: + - ์ด๋ฆ„: 2์ž ์ด์ƒ + - ์ „ํ™”๋ฒˆํ˜ธ: 010-0000-0000 ํ˜•์‹ ์ž๋™ ํฌ๋งท + - ๊ฐœ์ธ์ •๋ณด ๋™์˜: ํ•„์ˆ˜ ์ฒดํฌ +2. **์ „๋ฌธ๋ณด๊ธฐ**: Bottom Sheet๋กœ ๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ๋ฐฉ์นจ ํ‘œ์‹œ +3. **์ฐธ์—ฌํ•˜๊ธฐ**: + - ์ค‘๋ณต ์ฐธ์—ฌ ์ฒดํฌ + - ๋กœ๋”ฉ (1์ดˆ) + - ์• ๋‹ˆ๋ฉ”์ด์…˜ (์ฒดํฌ ์•„์ด์ฝ˜ + ์ถ•ํ•˜ ๋ฉ”์‹œ์ง€) +4. **ํ™•์ธ**: ์ฐธ์—ฌ ์™„๋ฃŒ ํ™”๋ฉด ๋‹ซ๊ธฐ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์ „์ฒด ํ™”๋ฉด (์ตœ์ ํ™”) +- Tablet: ์ค‘์•™ ์นด๋“œ (์ตœ๋Œ€ ๋„ˆ๋น„ 480px) +- Desktop: ์ค‘์•™ ์นด๋“œ (์ตœ๋Œ€ ๋„ˆ๋น„ 600px) + +**์ ‘๊ทผ์„ฑ** +- ์ด๋ฏธ์ง€ alt ํ…์ŠคํŠธ: "์‹ ๊ทœ๊ณ ๊ฐ ์œ ์น˜ ์ด๋ฒคํŠธ ํฌ์Šคํ„ฐ" +- ์ฐธ์—ฌ ๋ฒ„ํŠผ: aria-label="์ด๋ฒคํŠธ์— ์ฐธ์—ฌํ•˜๊ธฐ" +- ๊ฐœ์ธ์ •๋ณด ๋™์˜: ํ•„์ˆ˜ ์ฒดํฌ aria-required="true" + +--- + +#### 16-๋‹น์ฒจ์ž์ถ”์ฒจ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ๊ณต์ •ํ•œ ๋‹น์ฒจ์ž ์ž๋™ ์„ ์ • +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-PART-030 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: M/13 (ํ•„์ˆ˜, ์ค‘๊ฐ„ ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ๋‹น์ฒจ ์ธ์› ์„ค์ • +- ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์  ์˜ต์…˜ +- ์ž๋™ ์ถ”์ฒจ ์‹คํ–‰ +- ๋‹น์ฒจ์ž ๋ชฉ๋ก ํ‘œ์‹œ +- ์žฌ์ถ”์ฒจ ๊ธฐ๋Šฅ +- ์ถ”์ฒจ ์ด๋ ฅ ๋ณด๊ด€ + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +**Mobile (320-767px)** +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ๋‹น์ฒจ์ž ์ถ”์ฒจ ๐Ÿ‘ค โ”‚ Header +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ๐Ÿ“‹ ์ด๋ฒคํŠธ ์ •๋ณด โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ด๋ฒคํŠธ๋ช…: ์‹ ๊ทœ๊ณ ๊ฐ ํ• ์ธ ์ด๋ฒคํŠธ โ”‚ โ”‚ +โ”‚ โ”‚ ์ด ์ฐธ์—ฌ์ž: 127๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ ์ถ”์ฒจ ์ƒํƒœ: ์ถ”์ฒจ ์ „ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐ŸŽฏ ์ถ”์ฒจ ์„ค์ • โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋‹น์ฒจ ์ธ์› โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - โ”‚ โ”‚ 5 โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ˜‘ ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์  โ”‚ โ”‚ +โ”‚ โ”‚ (๊ฐ€์ค‘์น˜: 1.5๋ฐฐ) โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ„น๏ธ ์ถ”์ฒจ ๋ฐฉ์‹: ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฌด์ž‘์œ„ โ”‚ โ”‚ +โ”‚ โ”‚ ๋ชจ๋“  ์ถ”์ฒจ ๊ณผ์ •์€ ์ž๋™ ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐ŸŽฒ ์ถ”์ฒจ ์‹œ์ž‘ โ”‚ โ”‚ Primary Action +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“œ ์ถ”์ฒจ ์ด๋ ฅ (์ตœ๊ทผ 3๊ฑด) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ 2024-01-15 14:30 โ”‚ โ”‚ +โ”‚ โ”‚ ๋‹น์ฒจ์ž 5๋ช… | ๐Ÿ‘๏ธ ์ƒ์„ธ๋ณด๊ธฐ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ 2024-01-15 14:25 (์žฌ์ถ”์ฒจ) โ”‚ โ”‚ +โ”‚ โ”‚ ๋‹น์ฒจ์ž 5๋ช… | ๐Ÿ‘๏ธ ์ƒ์„ธ๋ณด๊ธฐ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„ โ”‚ Nav Bottom +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +[์ถ”์ฒจ ํ›„ ํ™”๋ฉด] +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ๋‹น์ฒจ์ž ์ถ”์ฒจ ๐Ÿ‘ค โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ๐ŸŽ‰ ์ถ”์ฒจ ์™„๋ฃŒ! โ”‚ +โ”‚ ์ด 127๋ช… ์ค‘ 5๋ช… ๋‹น์ฒจ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ† ๋‹น์ฒจ์ž ๋ชฉ๋ก โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ 1์œ„ (์‘๋ชจ๋ฒˆํ˜ธ: #00042) โ”‚ โ”‚ +โ”‚ โ”‚ ๊น€** (010-****-1234) โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ: ์šฐ๋ฆฌ๋™๋„คTV ๐ŸŒŸ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ 2์œ„ (์‘๋ชจ๋ฒˆํ˜ธ: #00089) โ”‚ โ”‚ +โ”‚ โ”‚ ์ด** (010-****-5678) โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ: SNS โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ 3์œ„ (์‘๋ชจ๋ฒˆํ˜ธ: #00103) โ”‚ โ”‚ +โ”‚ โ”‚ ๋ฐ•** (010-****-9012) โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ: ๋ง๊ณ ๋น„์ฆˆ ๐ŸŒŸ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ 4์œ„ (์‘๋ชจ๋ฒˆํ˜ธ: #00012) โ”‚ โ”‚ +โ”‚ โ”‚ ์ตœ** (010-****-3456) โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ: SNS โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ 5์œ„ (์‘๋ชจ๋ฒˆํ˜ธ: #00067) โ”‚ โ”‚ +โ”‚ โ”‚ ์ •** (010-****-7890) โ”‚ โ”‚ +โ”‚ โ”‚ ์ฐธ์—ฌ: ์šฐ๋ฆฌ๋™๋„คTV โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐ŸŒŸ ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์  ์ ์šฉ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ“ฅ ์—‘์…€๋‹ค์šด โ”‚ โ”‚ ๐Ÿ”„ ์žฌ์ถ”์ฒจ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋‹น์ฒจ์ž์—๊ฒŒ ์•Œ๋ฆผ ์ „์†ก โ”‚ โ”‚ Primary +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Tablet (768-1023px)** +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ๋‹น์ฒจ์ž ์ถ”์ฒจ ๐Ÿ‘ค โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ“‹ ์ด๋ฒคํŠธ ์ •๋ณด โ”‚ โ”‚ ๐ŸŽฏ ์ถ”์ฒจ ์„ค์ • โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ์ด๋ฒคํŠธ๋ช…: โ”‚ โ”‚ ๋‹น์ฒจ ์ธ์› โ”‚ โ”‚ +โ”‚ โ”‚ ์‹ ๊ทœ๊ณ ๊ฐ ํ• ์ธ โ”‚ โ”‚ โ”Œโ”€โ” โ”Œโ”€โ” โ”Œโ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚-โ”‚ โ”‚5โ”‚ โ”‚+โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ์ด ์ฐธ์—ฌ์ž: โ”‚ โ”‚ โ””โ”€โ”˜ โ””โ”€โ”˜ โ””โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ 127๋ช… โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ˜‘ ๋งค์žฅ ๋ฐฉ๋ฌธ โ”‚ โ”‚ +โ”‚ โ”‚ ์ถ”์ฒจ ์ƒํƒœ: โ”‚ โ”‚ ๊ฐ€์‚ฐ์  1.5๋ฐฐ โ”‚ โ”‚ +โ”‚ โ”‚ ์ถ”์ฒจ ์ „ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ„น๏ธ ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฌด์ž‘์œ„ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ ์ถ”์ฒจ ๊ณผ์ • ์ž๋™ ๊ธฐ๋ก โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ๐ŸŽฒ ์ถ”์ฒจ ์‹œ์ž‘ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“œ ์ถ”์ฒจ ์ด๋ ฅ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๋‚ ์งœ ๋‹น์ฒจ์ž ์ƒํƒœ ์ƒ์„ธ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ 2024-01-15 5๋ช… ์™„๋ฃŒ ๐Ÿ‘๏ธ โ”‚ โ”‚ +โ”‚ โ”‚ 2024-01-15 5๋ช… ์žฌ์ถ”์ฒจ ๐Ÿ‘๏ธ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Desktop (1024px+)** +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ๋‹น์ฒจ์ž ์ถ”์ฒจ ๐Ÿ‘ค โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ“‹ ์ด๋ฒคํŠธ ์ •๋ณด โ”‚ โ”‚ ๐ŸŽฏ ์ถ”์ฒจ ์„ค์ • โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ์ด๋ฒคํŠธ๋ช…: โ”‚ โ”‚ ๋‹น์ฒจ ์ธ์› โ”‚ โ”‚ +โ”‚ โ”‚ ์‹ ๊ทœ๊ณ ๊ฐ ํ• ์ธ ์ด๋ฒคํŠธ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ - โ”‚ โ”‚ 5 โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„: โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ 2024-01-01 ~ 01-14 โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ˜‘ ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์  โ”‚ โ”‚ +โ”‚ โ”‚ ์ด ์ฐธ์—ฌ์ž: 127๋ช… โ”‚ โ”‚ (๊ฐ€์ค‘์น˜: 1.5๋ฐฐ) โ”‚ โ”‚ +โ”‚ โ”‚ - ์šฐ๋ฆฌ๋™๋„คTV: 45๋ช… โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ - ๋ง๊ณ ๋น„์ฆˆ: 32๋ช… โ”‚ โ”‚ โ„น๏ธ ์ถ”์ฒจ ๋ฐฉ์‹ โ”‚ โ”‚ +โ”‚ โ”‚ - ์ง€๋‹ˆTV: 28๋ช… โ”‚ โ”‚ โ€ข ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฌด์ž‘์œ„ โ”‚ โ”‚ +โ”‚ โ”‚ - SNS: 22๋ช… โ”‚ โ”‚ โ€ข ์ถ”์ฒจ ๊ณผ์ • ์ž๋™ ๊ธฐ๋ก โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ€ข ๊ณต์ •์„ฑ ๋ณด์žฅ โ”‚ โ”‚ +โ”‚ โ”‚ ์ถ”์ฒจ ์ƒํƒœ: ์ถ”์ฒจ ์ „ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ ๐ŸŽฒ ์ถ”์ฒจ ์‹œ์ž‘ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“œ ์ถ”์ฒจ ์ด๋ ฅ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ์ถ”์ฒจ ์ผ์‹œ ๋‹น์ฒจ์ž ์žฌ์ถ”์ฒจ ์ถ”์ฒจ๋ฐฉ์‹ ์ƒ์„ธ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ +โ”‚ โ”‚ 2024-01-15 14:30 5๋ช… - ๋ฌด์ž‘์œ„ ๐Ÿ‘๏ธ โ”‚ โ”‚ +โ”‚ โ”‚ 2024-01-15 14:25 5๋ช… Y ๋ฌด์ž‘์œ„ ๐Ÿ‘๏ธ โ”‚ โ”‚ +โ”‚ โ”‚ 2024-01-15 14:20 5๋ช… - ๋ฌด์ž‘์œ„ ๐Ÿ‘๏ธ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +[์ถ”์ฒจ ํ›„ ํ™”๋ฉด - Desktop] +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ๋‹น์ฒจ์ž ์ถ”์ฒจ ๊ฒฐ๊ณผ ๐Ÿ‘ค โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ๐ŸŽ‰ ์ถ”์ฒจ ์™„๋ฃŒ! ์ด 127๋ช… ์ค‘ 5๋ช… ๋‹น์ฒจ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ† ๋‹น์ฒจ์ž ๋ชฉ๋ก โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๐Ÿ“Š ํ†ต๊ณ„ โ”‚ โ”‚ +โ”‚ โ”‚ ์ˆœ์œ„ ์‘๋ชจ๋ฒˆํ˜ธ ์ด๋ฆ„ ์ „ํ™”๋ฒˆํ˜ธ ์ฐธ์—ฌ๊ฒฝ๋กœโ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ ์ฑ„๋„๋ณ„ ๋‹น์ฒจ ๋ถ„ํฌ: โ”‚ โ”‚ +โ”‚ โ”‚ 1์œ„ #00042 ๊น€** 010-****-1234 ์šฐ๋ฆฌ๋™๋„คTV ๐ŸŒŸโ”‚ โ”‚ ์šฐ๋ฆฌ๋™๋„คTV: 2๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ 2์œ„ #00089 ์ด** 010-****-5678 SNS โ”‚ โ”‚ ๋ง๊ณ ๋น„์ฆˆ: 1๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ 3์œ„ #00103 ๋ฐ•** 010-****-9012 ๋ง๊ณ ๋น„์ฆˆ ๐ŸŒŸโ”‚ โ”‚ SNS: 2๋ช… โ”‚ โ”‚ +โ”‚ โ”‚ 4์œ„ #00012 ์ตœ** 010-****-3456 SNS โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ 5์œ„ #00067 ์ •** 010-****-7890 ์šฐ๋ฆฌ๋™๋„คTV โ”‚ โ”‚ ๊ฐ€์‚ฐ์  ์ ์šฉ: 2๋ช… โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ์ถ”์ฒจ ์ผ์‹œ: โ”‚ โ”‚ +โ”‚ ๐ŸŒŸ ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์  ์ ์šฉ โ”‚ 2024-01-15 14:30 โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ ๐Ÿ“ฅ ์—‘์…€๋‹ค์šด โ”‚ โ”‚ ๐Ÿ”„ ์žฌ์ถ”์ฒจ โ”‚ โ”‚ ๋‹น์ฒจ์ž์—๊ฒŒ ์•Œ๋ฆผ ์ „์†ก โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** + +**์ถ”์ฒจ ์ „ ์„ค์ •** +- ๋‹น์ฒจ ์ธ์›: +/- ๋ฒ„ํŠผ์œผ๋กœ ์กฐ์ • (1~100๋ช…) +- ๊ฐ€์‚ฐ์  ์˜ต์…˜: ์ฒดํฌ๋ฐ•์Šค ํ† ๊ธ€ +- ์ถ”์ฒจ ์‹œ์ž‘: ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ โ†’ "์ด 127๋ช… ์ค‘ 5๋ช…์„ ์ถ”์ฒจํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + +**์ถ”์ฒจ ์‹คํ–‰** +- ๋กœ๋”ฉ ์• ๋‹ˆ๋ฉ”์ด์…˜: 3์ดˆ๊ฐ„ ์Šฌ๋กฏ๋จธ์‹  ํšจ๊ณผ +- ์ง„ํ–‰ ์ƒํ™ฉ: "์ถ”์ฒจ ์ค‘... (๋‚œ์ˆ˜ ์ƒ์„ฑ ์ค‘)" โ†’ "๋‹น์ฒจ์ž ์„ ์ • ์ค‘..." โ†’ "์™„๋ฃŒ!" +- ์ถ”์ฒจ ์™„๋ฃŒ: ๋‹น์ฒจ์ž ๋ชฉ๋ก ํŽ˜์ด๋“œ์ธ ์• ๋‹ˆ๋ฉ”์ด์…˜ + +**๋‹น์ฒจ์ž ๋ชฉ๋ก** +- ์ˆœ์œ„๋ณ„ ํ•˜์ด๋ผ์ดํŠธ: 1์œ„(๊ธˆ์ƒ‰), 2์œ„(์€์ƒ‰), 3์œ„(๋™์ƒ‰) +- ๊ฐ€์‚ฐ์  ์ ์šฉ ํ‘œ์‹œ: ๐ŸŒŸ ์•„์ด์ฝ˜์œผ๋กœ ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ตฌ๋ถ„ +- ๊ฐœ์ธ์ •๋ณด ๋งˆ์Šคํ‚น: ์ด๋ฆ„(๊น€**), ์ „ํ™”๋ฒˆํ˜ธ(010-****-1234) + +**์žฌ์ถ”์ฒจ** +- ์žฌ์ถ”์ฒจ ๋ฒ„ํŠผ: ๊ฒฝ๊ณ  ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ + - "์žฌ์ถ”์ฒจ ์‹œ ํ˜„์žฌ ๋‹น์ฒจ์ž ์ •๋ณด๊ฐ€ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. ๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + - "์ด์ „ ์ถ”์ฒจ ์ด๋ ฅ์€ ๋ณด๊ด€๋ฉ๋‹ˆ๋‹ค" +- ํ™•์ธ ์‹œ: ์ƒˆ๋กœ์šด ์ถ”์ฒจ ์‹คํ–‰, ์ด์ „ ๊ฒฐ๊ณผ๋Š” ์ด๋ ฅ์— "(์žฌ์ถ”์ฒจ)" ํ‘œ์‹œ + +**์•Œ๋ฆผ ์ „์†ก** +- ์•Œ๋ฆผ ์ „์†ก ๋ฒ„ํŠผ: ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ ํ‘œ์‹œ + - "5๋ช…์˜ ๋‹น์ฒจ์ž์—๊ฒŒ SMS ์•Œ๋ฆผ์„ ์ „์†กํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + - "์˜ˆ์ƒ ๋น„์šฉ: 500์› (100์›/๊ฑด)" +- ์ „์†ก ์™„๋ฃŒ: ํ† ์ŠคํŠธ ๋ฉ”์‹œ์ง€ "์•Œ๋ฆผ์ด ์ „์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + +**์—‘์…€ ๋‹ค์šด๋กœ๋“œ** +- ํŒŒ์ผ๋ช…: `๋‹น์ฒจ์ž๋ชฉ๋ก_[์ด๋ฒคํŠธ๋ช…]_[์ถ”์ฒจ์ผ์‹œ].xlsx` +- ํฌํ•จ ์ •๋ณด: ์ˆœ์œ„, ์‘๋ชจ๋ฒˆํ˜ธ, ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ฐธ์—ฌ๊ฒฝ๋กœ, ๊ฐ€์‚ฐ์ ์—ฌ๋ถ€, ์ถ”์ฒจ์ผ์‹œ + +**์ถ”์ฒจ ์ด๋ ฅ ์ƒ์„ธ๋ณด๊ธฐ** +- ํด๋ฆญ ์‹œ: ๋ชจ๋‹ฌ๋กœ ํ•ด๋‹น ์ถ”์ฒจ์˜ ๋‹น์ฒจ์ž ๋ชฉ๋ก ํ‘œ์‹œ +- ์žฌ์ถ”์ฒจ ์—ฌ๋ถ€, ์ถ”์ฒจ ์„ค์ • ์ •๋ณด ํฌํ•จ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** + +**Mobile (320-767px)** +- 1์—ด ๋ ˆ์ด์•„์›ƒ: ์ด๋ฒคํŠธ ์ •๋ณด โ†’ ์ถ”์ฒจ ์„ค์ • โ†’ ์ถ”์ฒจ ๋ฒ„ํŠผ โ†’ ์ด๋ ฅ +- ๋‹น์ฒจ์ž ๋ชฉ๋ก: ์นด๋“œํ˜• (1๋ช…์”ฉ) +- ์ถ”์ฒจ ์„ค์ •: ์ „์ฒด ๋„ˆ๋น„ ์‚ฌ์šฉ +- ๋ฒ„ํŠผ: ์Šคํƒํ˜• ๋ฐฐ์น˜ (์„ธ๋กœ ์ •๋ ฌ) + +**Tablet (768-1023px)** +- 2์—ด ๋ ˆ์ด์•„์›ƒ: ์ด๋ฒคํŠธ ์ •๋ณด | ์ถ”์ฒจ ์„ค์ • +- ๋‹น์ฒจ์ž ๋ชฉ๋ก: ํ…Œ์ด๋ธ” ํ˜•์‹ (๊ฐ„์†Œํ™”) +- ์ถ”์ฒจ ์ด๋ ฅ: ํ…Œ์ด๋ธ” ํ˜•์‹ +- ๋ฒ„ํŠผ: ์ˆ˜ํ‰ ๋ฐฐ์น˜ + +**Desktop (1024px+)** +- ์ตœ์ ํ™”๋œ ํ…Œ์ด๋ธ” ๋ ˆ์ด์•„์›ƒ +- ๋‹น์ฒจ์ž ๋ชฉ๋ก: ์ „์ฒด ์ •๋ณด ํ‘œ์‹œ +- ์šฐ์ธก์— ํ†ต๊ณ„ ํŒจ๋„ ์ถ”๊ฐ€ +- ์ถ”์ฒจ ์ด๋ ฅ: ์ „์ฒด ์ •๋ณด ํ‘œ์‹œ +- ๋ฒ„ํŠผ: ์ˆ˜ํ‰ ๋ฐฐ์น˜, ์šฐ์ธก ์ •๋ ฌ + +**ํ„ฐ์น˜/๋งˆ์šฐ์Šค ์ตœ์ ํ™”** +- Mobile: ํ„ฐ์น˜ ์˜์—ญ ์ตœ์†Œ 44px +- Desktop: ํ˜ธ๋ฒ„ ํšจ๊ณผ, ํˆดํŒ ํ‘œ์‹œ +- ์žฌ์ถ”์ฒจ/์•Œ๋ฆผ ๋ฒ„ํŠผ: ๊ฒฝ๊ณ ์ƒ‰ (์ฃผ์˜ ํ•„์š”) + +**์„ฑ๋Šฅ ์ตœ์ ํ™”** +- ์ถ”์ฒจ ์‹คํ–‰: ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ์ฒ˜๋ฆฌ (๊ณต์ •์„ฑ ๋ณด์žฅ) +- ๋กœ๋”ฉ ์ƒํƒœ: ์Šค์ผˆ๋ ˆํ†ค UI ํ‘œ์‹œ +- ๋‹น์ฒจ์ž ๋ชฉ๋ก: ํŽ˜์ด์ง• (50๋ช… ์ด์ƒ ์‹œ) +- ์ถ”์ฒจ ์ด๋ ฅ: ์ตœ๊ทผ 10๊ฑด๋งŒ ํ‘œ์‹œ, "๋”๋ณด๊ธฐ" ์˜ต์…˜ + +**์ ‘๊ทผ์„ฑ** +- ์ถ”์ฒจ ์‹œ์ž‘ ๋ฒ„ํŠผ: aria-label="์ถ”์ฒจ ์‹œ์ž‘, 127๋ช… ์ค‘ 5๋ช… ์„ ์ •" +- ์žฌ์ถ”์ฒจ ๋ฒ„ํŠผ: aria-label="์žฌ์ถ”์ฒจ, ์ฃผ์˜ ํ•„์š”" +- ๋‹น์ฒจ์ž ์ •๋ณด: ์Šคํฌ๋ฆฐ๋ฆฌ๋”์šฉ ์ „์ฒด ์ •๋ณด ์ œ๊ณต +- ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜: Tab, Enter๋กœ ๋ชจ๋“  ๊ธฐ๋Šฅ ์ ‘๊ทผ ๊ฐ€๋Šฅ + +--- + +### 5.5 ์„ฑ๊ณผ ๋ถ„์„ + +#### 17-์‹ค์‹œ๊ฐ„๋Œ€์‹œ๋ณด๋“œ + +**๊ฐœ์š”** +- **๋ชฉ์ **: ์ด๋ฒคํŠธ ์„ฑ๊ณผ ์‹ค์‹œ๊ฐ„ ์ธก์ • ๋ฐ ๋ถ„์„ +- **๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ**: UFR-ANAL-010 +- **๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„**: M/34 (ํ•„์ˆ˜, ์ตœ๊ณ  ๋ณต์žก๋„) + +**์ฃผ์š” ๊ธฐ๋Šฅ** +- ์š”์•ฝ ์นด๋“œ 4๊ฐœ (์ฐธ์—ฌ์ž, ๋น„์šฉ, ์ˆ˜์ต, ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ ) +- ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ (ํŒŒ์ด ์ฐจํŠธ) +- ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด (๋ผ์ธ ์ฐจํŠธ) +- ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  ์ƒ์„ธ ๋ถ„์„ (ํ…Œ์ด๋ธ”) +- ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ถ„์„ (๋ง‰๋Œ€ ์ฐจํŠธ) +- ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (5๋ถ„ ๊ฐ„๊ฒฉ) + +**UI ๊ตฌ์„ฑ์š”์†Œ** + +*Mobile (320-767px)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ˜ฐ ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ๐Ÿ‘คโ”‚ โ† Header ์ถ”๊ฐ€ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ ๐Ÿ“Š ์š”์•ฝ (์‹ค์‹œ๊ฐ„) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ”‚์ฐธ์—ฌ์ž์ˆ˜ โ”‚โ”‚์ด ๋น„์šฉ โ”‚โ”‚ +โ”‚ โ”‚ 128๋ช… โ”‚โ”‚ 30๋งŒ์› โ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ”‚์˜ˆ์ƒ์ˆ˜์ต โ”‚โ”‚ํˆฌ์ž๋Œ€๋น„โ”‚โ”‚ +โ”‚ โ”‚ 135๋งŒ์›โ”‚โ”‚์ˆ˜์ต๋ฅ  โ”‚โ”‚ +โ”‚ โ”‚ โ”‚โ”‚ 450% โ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“บ ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [ํŒŒ์ด ์ฐจํŠธ] โ”‚ โ”‚ +โ”‚ โ”‚ ์šฐ๋ฆฌ๋™๋„คTV: 45% โ”‚ โ”‚ +โ”‚ โ”‚ ๋ง๊ณ ๋น„์ฆˆ: 30% โ”‚ โ”‚ +โ”‚ โ”‚ SNS: 25% โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ“ˆ ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ [๋ผ์ธ ์ฐจํŠธ] โ”‚ โ”‚ +โ”‚ โ”‚ ํ”ผํฌ: ์˜คํ›„ 2-4์‹œ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ’ฐ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  ์ƒ์„ธโ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚์ด ๋น„์šฉ: 30๋งŒ์› โ”‚ โ”‚ +โ”‚ โ”‚โ€ข ๊ฒฝํ’ˆ: 25๋งŒ์› โ”‚ โ”‚ +โ”‚ โ”‚โ€ข ์ฑ„๋„: 5๋งŒ์› โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚์˜ˆ์ƒ ์ˆ˜์ต: 135๋งŒ์›โ”‚ โ”‚ +โ”‚ โ”‚โ€ข ๋งค์ถœ ์ฆ๊ฐ€: 100๋งŒโ”‚ โ”‚ +โ”‚ โ”‚โ€ข ์‹ ๊ทœ LTV: 35๋งŒ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : โ”‚ โ”‚ +โ”‚ โ”‚(135-30)/30ร—100 โ”‚ โ”‚ +โ”‚ โ”‚= 450% โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ ๐Ÿ‘ฅ ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚[๋ง‰๋Œ€ ์ฐจํŠธ] โ”‚ โ”‚ +โ”‚ โ”‚์—ฐ๋ น: 20๋Œ€ 35% โ”‚ โ”‚ +โ”‚ โ”‚ 30๋Œ€ 40% โ”‚ โ”‚ +โ”‚ โ”‚ 40๋Œ€ 25% โ”‚ โ”‚ +โ”‚ โ”‚์„ฑ๋ณ„: ์—ฌ 60% โ”‚ โ”‚ +โ”‚ โ”‚ ๋‚จ 40% โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ํ™ˆ ์ด๋ฒคํŠธ ๋ถ„์„ ํ”„๋กœํ•„โ”‚ โ† Nav Bottom ์ถ”๊ฐ€ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +*Desktop (1024px+)* +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ† ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๐Ÿ“Š ์š”์•ฝ (์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚์ฐธ์—ฌ์žโ”‚โ”‚๋น„์šฉ โ”‚โ”‚์ˆ˜์ต โ”‚โ”‚ํˆฌ์ž๋Œ€โ”‚โ”‚ โ”‚ +โ”‚ โ”‚128๋ช…โ”‚โ”‚30๋งŒ โ”‚โ”‚135๋งŒโ”‚โ”‚๋น„์ˆ˜์ตโ”‚โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚โ”‚ โ”‚โ”‚ โ”‚โ”‚๋ฅ 450%โ”‚โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ”‚๐Ÿ“บ ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ โ”‚โ”‚๐Ÿ“ˆ ์‹œ๊ฐ„๋Œ€๋ณ„ ์ถ”์ดโ”‚โ”‚ +โ”‚ โ”‚ โ”‚โ”‚ โ”‚โ”‚ +โ”‚ โ”‚ [ํŒŒ์ด ์ฐจํŠธ] โ”‚โ”‚ [๋ผ์ธ ์ฐจํŠธ] โ”‚โ”‚ +โ”‚ โ”‚ โ”‚โ”‚ โ”‚โ”‚ +โ”‚ โ”‚ โ€ข ์šฐ๋ฆฌ๋™๋„คTV 45% โ”‚โ”‚ ํ”ผํฌ: ์˜คํ›„2-4 โ”‚โ”‚ +โ”‚ โ”‚ โ€ข ๋ง๊ณ ๋น„์ฆˆ 30% โ”‚โ”‚ โ”‚โ”‚ +โ”‚ โ”‚ โ€ข SNS 25% โ”‚โ”‚ โ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”โ”‚ +โ”‚ โ”‚๐Ÿ’ฐ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  โ”‚โ”‚๐Ÿ‘ฅ ์ฐธ์—ฌ์ž ํ”„๋กœํ•„โ”‚โ”‚ +โ”‚ โ”‚ โ”‚โ”‚ โ”‚โ”‚ +โ”‚ โ”‚ [์ƒ์„ธ ํ…Œ์ด๋ธ”] โ”‚โ”‚ [์ฐจํŠธ] โ”‚โ”‚ +โ”‚ โ”‚ โ”‚โ”‚ โ”‚โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**์ธํ„ฐ๋ž™์…˜** +1. **์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ**: + - 5๋ถ„ ๊ฐ„๊ฒฉ ์ž๋™ ํด๋ง + - ๋ณ€๊ฒฝ ์‚ฌํ•ญ ํ•˜์ด๋ผ์ดํŠธ (์ˆซ์ž ์นด์šดํ„ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜) +2. **์ฐจํŠธ ์ธํ„ฐ๋ž™์…˜**: + - ํŒŒ์ด ์ฐจํŠธ: ํ˜ธ๋ฒ„/ํด๋ฆญ ์‹œ ์ƒ์„ธ ์ •๋ณด ํˆดํŒ + - ๋ผ์ธ ์ฐจํŠธ: ํ˜ธ๋ฒ„ ์‹œ ์‹œ๊ฐ„๋Œ€๋ณ„ ์ •ํ™•ํ•œ ์ˆ˜์น˜ ํ‘œ์‹œ +3. **์ƒˆ๋กœ๊ณ ์นจ**: Pull to Refresh (๋ชจ๋ฐ”์ผ) +4. **์ƒ์„ธ๋ณด๊ธฐ**: ๊ฐ ์„น์…˜ ํด๋ฆญ ์‹œ ํ™•์žฅ/์ถ•์†Œ + +**๋ฐ˜์‘ํ˜• ์ฒ˜๋ฆฌ** +- Mobile: ์„ธ๋กœ ์Šคํฌ๋กค, ์นด๋“œ 1์—ด, ๋‹จ์ˆœํ•œ ์ฐจํŠธ (๋ง‰๋Œ€/ํŒŒ์ด) +- Tablet: ์นด๋“œ 2์—ด, ์ฐจํŠธ ํ™•์žฅ +- Desktop: ์นด๋“œ 4์—ด, ๋ณต์žกํ•œ ์ฐจํŠธ (๋ผ์ธ/์˜์—ญ), 2์—ด ๋ ˆ์ด์•„์›ƒ + +--- + +## 6. ํ™”๋ฉด ๊ฐ„ ์ „ํ™˜ ๋ฐ ๋„ค๋น„๊ฒŒ์ด์…˜ + +### 6.1 ๋„ค๋น„๊ฒŒ์ด์…˜ ๊ตฌ์กฐ + +#### Mobile (Bottom Navigation) +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ”‚ +โ”‚ [ํ™”๋ฉด ์ฝ˜ํ…์ธ ] โ”‚ +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚[ํ™ˆ][์ด๋ฒคํŠธ][๋ถ„์„][ํ”„๋กœํ•„]โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**๋ฉ”๋‰ด ํ•ญ๋ชฉ**: +- **ํ™ˆ**: ๋Œ€์‹œ๋ณด๋“œ (05-๋Œ€์‹œ๋ณด๋“œ) +- **์ด๋ฒคํŠธ**: ์ด๋ฒคํŠธ ๋ชฉ๋ก (06-์ด๋ฒคํŠธ๋ชฉ๋ก) +- **๋ถ„์„**: ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ (17-์‹ค์‹œ๊ฐ„๋Œ€์‹œ๋ณด๋“œ) +- **ํ”„๋กœํ•„**: ํ”„๋กœํ•„ ํŽธ์ง‘ (03-ํ”„๋กœํ•„ํŽธ์ง‘) + +#### Desktop (Side Navigation) +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ [๋กœ๊ณ ] [ํ”„๋กœํ•„] โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ ๋ฉ”๋‰ด โ”‚ [ํ™”๋ฉด ์ฝ˜ํ…์ธ ] โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚โ€ข ํ™ˆ โ”‚ โ”‚ +โ”‚โ€ข ์ด๋ฒคํŠธโ”‚ โ”‚ +โ”‚โ€ข ๋ถ„์„ โ”‚ โ”‚ +โ”‚โ€ข ํ”„๋กœํ•„โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 6.2 ํ™”๋ฉด ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ + +**Forward Navigation** (๋‹ค์Œ ๋‹จ๊ณ„): +- Slide Left (์™ผ์ชฝ์œผ๋กœ ์Šฌ๋ผ์ด๋“œ) +- Duration: 300ms +- Easing: ease-out + +**Backward Navigation** (์ด์ „ ๋‹จ๊ณ„): +- Slide Right (์˜ค๋ฅธ์ชฝ์œผ๋กœ ์Šฌ๋ผ์ด๋“œ) +- Duration: 300ms +- Easing: ease-out + +**Tab Navigation** (ํƒญ ์ „ํ™˜): +- Fade In/Out +- Duration: 200ms +- Easing: ease-in-out + +**Modal/Dialog**: +- Fade In + Scale Up (๋“ฑ์žฅ) +- Fade Out + Scale Down (ํ‡ด์žฅ) +- Duration: 250ms +- Easing: cubic-bezier(0.4, 0, 0.2, 1) + +### 6.3 Floating Action Button (FAB) + +**์œ„์น˜**: ์šฐ์ธก ํ•˜๋‹จ +**๊ธฐ๋Šฅ**: "์ƒˆ ์ด๋ฒคํŠธ ๋งŒ๋“ค๊ธฐ" +**๋™์ž‘**: 07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ ํ™”๋ฉด ์ด๋™ + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ” + โ”‚ + โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 7. ๋ฐ˜์‘ํ˜• ์„ค๊ณ„ ์ „๋žต + +### 7.1 ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ + +```css +/* Mobile First Breakpoints */ + +/* Mobile: 320px ~ 767px (๊ธฐ๋ณธ ์Šคํƒ€์ผ) */ +@media (min-width: 768px) { + /* Tablet: 768px ~ 1023px */ +} + +@media (min-width: 1024px) { + /* Desktop: 1024px ~ 1439px */ +} + +@media (min-width: 1440px) { + /* Large Desktop: 1440px ~ */ + /* ์ตœ๋Œ€ ๋„ˆ๋น„ 1280px, ์ค‘์•™ ์ •๋ ฌ */ +} +``` + +### 7.2 ๋ ˆ์ด์•„์›ƒ ์ „๋žต + +#### Grid System + +**Mobile**: +- 1์—ด ๋ ˆ์ด์•„์›ƒ +- ์ขŒ์šฐ ์—ฌ๋ฐฑ: 16px +- ์š”์†Œ ๊ฐ„ ๊ฐ„๊ฒฉ: 16px + +**Tablet**: +- 2์—ด ๋ ˆ์ด์•„์›ƒ (50:50 ๋˜๋Š” 30:70) +- ์ขŒ์šฐ ์—ฌ๋ฐฑ: 24px +- ์š”์†Œ ๊ฐ„ ๊ฐ„๊ฒฉ: 24px + +**Desktop**: +- 3์—ด ๋ ˆ์ด์•„์›ƒ (33:33:33 ๋˜๋Š” 20:60:20) +- ์ขŒ์šฐ ์—ฌ๋ฐฑ: 32px +- ์š”์†Œ ๊ฐ„ ๊ฐ„๊ฒฉ: 32px +- ์ตœ๋Œ€ ๋„ˆ๋น„: 1280px (์ค‘์•™ ์ •๋ ฌ) + +### 7.3 ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ + +```css +/* Mobile */ +h1: 24px / 1.2 / bold +h2: 20px / 1.3 / bold +h3: 18px / 1.4 / semibold +body: 14px / 1.5 / regular +small: 12px / 1.5 / regular + +/* Desktop */ +h1: 32px / 1.2 / bold +h2: 28px / 1.3 / bold +h3: 24px / 1.4 / semibold +body: 16px / 1.5 / regular +small: 14px / 1.5 / regular +``` + +### 7.4 ํ„ฐ์น˜ ์˜์—ญ + +**Mobile**: +- ์ตœ์†Œ ํ„ฐ์น˜ ์˜์—ญ: 44x44px (์• ํ”Œ ๊ถŒ์žฅ) +- ๋ฒ„ํŠผ ๋†’์ด: 48px +- ์ž…๋ ฅ ํ•„๋“œ ๋†’์ด: 48px + +**Desktop**: +- ์ตœ์†Œ ํด๋ฆญ ์˜์—ญ: 32x32px +- ๋ฒ„ํŠผ ๋†’์ด: 40px +- ์ž…๋ ฅ ํ•„๋“œ ๋†’์ด: 40px + +--- + +## 8. ์ ‘๊ทผ์„ฑ ๋ณด์žฅ ๋ฐฉ์•ˆ + +### 8.1 WCAG 2.1 AA ์ค€์ˆ˜ + +**์ƒ‰์ƒ ๋Œ€๋น„**: +- ์ผ๋ฐ˜ ํ…์ŠคํŠธ: 4.5:1 +- ํฐ ํ…์ŠคํŠธ (18px ์ด์ƒ): 3:1 +- UI ์ปดํฌ๋„ŒํŠธ: 3:1 + +**ํ‚ค๋ณด๋“œ ๋„ค๋น„๊ฒŒ์ด์…˜**: +- ๋ชจ๋“  ๊ธฐ๋Šฅ Tab ํ‚ค๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅ +- Focus Indicator ๋ช…ํ™•ํžˆ ํ‘œ์‹œ (2px ํŒŒ๋ž€์ƒ‰ ํ…Œ๋‘๋ฆฌ) +- Escape ํ‚ค๋กœ ๋ชจ๋‹ฌ/๋‹ค์ด์–ผ๋กœ๊ทธ ๋‹ซ๊ธฐ + +**์Šคํฌ๋ฆฐ ๋ฆฌ๋”**: +- ๋ชจ๋“  ์ด๋ฏธ์ง€ alt ํ…์ŠคํŠธ ์ œ๊ณต +- ํผ Label๊ณผ Input ์—ฐ๊ฒฐ (for/id) +- ARIA ์†์„ฑ ํ™œ์šฉ: + - aria-label + - aria-describedby + - aria-live (๋™์  ์ฝ˜ํ…์ธ ) + - aria-required (ํ•„์ˆ˜ ์ž…๋ ฅ) + +### 8.2 ์ ‘๊ทผ์„ฑ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +**์ด๋ฏธ์ง€**: +- [ ] ๋ชจ๋“  ์ด๋ฏธ์ง€ alt ์†์„ฑ ์ œ๊ณต +- [ ] ์žฅ์‹์šฉ ์ด๋ฏธ์ง€ alt="" ์ฒ˜๋ฆฌ + +**ํผ**: +- [ ] Label๊ณผ Input ์—ฐ๊ฒฐ +- [ ] ํ•„์ˆ˜ ํ•„๋“œ ํ‘œ์‹œ (*, aria-required) +- [ ] ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์ฝ๊ธฐ + +**๋„ค๋น„๊ฒŒ์ด์…˜**: +- [ ] ํ‚ค๋ณด๋“œ๋งŒ์œผ๋กœ ๋ชจ๋“  ๊ธฐ๋Šฅ ์ ‘๊ทผ ๊ฐ€๋Šฅ +- [ ] Focus ์ˆœ์„œ ๋…ผ๋ฆฌ์  +- [ ] Skip Navigation ๋งํฌ ์ œ๊ณต + +**์ฝ˜ํ…์ธ **: +- [ ] ์ œ๋ชฉ ๊ณ„์ธต ๊ตฌ์กฐ (h1 > h2 > h3) +- [ ] ์ƒ‰์ƒ๋งŒ์œผ๋กœ ์ •๋ณด ์ „๋‹ฌํ•˜์ง€ ์•Š์Œ +- [ ] ์ถฉ๋ถ„ํ•œ ์ƒ‰์ƒ ๋Œ€๋น„ + +--- + +## 9. ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐฉ์•ˆ + +### 9.1 ์ด๋ฏธ์ง€ ์ตœ์ ํ™” + +**Lazy Loading**: +```html +์ด๋ฒคํŠธ ํฌ์Šคํ„ฐ +``` + +**Responsive Images**: +```html +์ด๋ฒคํŠธ ํฌ์Šคํ„ฐ +``` + +**WebP ํฌ๋งท**: +- ์••์ถ•๋ฅ  30% ํ–ฅ์ƒ +- Fallback: JPEG/PNG + +**์ด๋ฏธ์ง€ CDN**: +- Cloudflare/AWS CloudFront +- ์ž๋™ ๋ฆฌ์‚ฌ์ด์ฆˆ ๋ฐ ํฌ๋งท ๋ณ€ํ™˜ + +### 9.2 ์ฝ”๋“œ ์ตœ์ ํ™” + +**Code Splitting**: +```javascript +// ํŽ˜์ด์ง€๋ณ„ ๋ฒˆ๋“ค ๋ถ„๋ฆฌ +const Dashboard = lazy(() => import('./Dashboard')); +const EventCreate = lazy(() => import('./EventCreate')); +``` + +**Tree Shaking**: +- ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ ์ œ๊ฑฐ +- ES6 Module ์‚ฌ์šฉ + +**Minification**: +- JS/CSS ์••์ถ• +- Gzip/Brotli ์••์ถ• + +### 9.3 ๋กœ๋”ฉ ์ „๋žต + +**Skeleton Screen**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ โ”‚ โ”‚ โ† Skeleton Card +โ”‚ โ”‚โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Critical CSS**: +- Above the Fold CSS ์ธ๋ผ์ธ ์‚ฝ์ž… +- ๋‚˜๋จธ์ง€ CSS ๋น„๋™๊ธฐ ๋กœ๋“œ + +**Defer/Async Script**: +```html + + +``` + +### 9.4 ๋ฐ์ดํ„ฐ ์ตœ์ ํ™” + +**Pagination**: +- ๋ชฉ๋ก์€ 20๊ฐœ์”ฉ ํŽ˜์ด์ง• +- Infinite Scroll (๋ชจ๋ฐ”์ผ) + +**Debouncing**: +```javascript +// ๊ฒ€์ƒ‰ ์ž…๋ ฅ ์‹œ 500ms ๋Œ€๊ธฐ ํ›„ API ํ˜ธ์ถœ +const debouncedSearch = debounce(search, 500); +``` + +**LocalStorage**: +- ์ž„์‹œ ์ €์žฅ (์ด๋ฒคํŠธ ์ƒ์„ฑ ์ง„ํ–‰ ์ƒํƒœ) +- ์˜คํ”„๋ผ์ธ ๋Œ€์‘ + +### 9.5 ์„ฑ๋Šฅ ๋ชฉํ‘œ + +**Core Web Vitals**: +- LCP (Largest Contentful Paint): < 2.5s +- FID (First Input Delay): < 100ms +- CLS (Cumulative Layout Shift): < 0.1 + +**์ถ”๊ฐ€ ์ง€ํ‘œ**: +- First Contentful Paint: < 1.5s +- Time to Interactive: < 3s +- Lighthouse Score: > 90 +- ์ด๋ฏธ์ง€ ๋กœ๋”ฉ: < 1s (per image) +- API ์‘๋‹ต: < 500ms (ํ‰๊ท ) + +--- + +## 10. ๋ณ€๊ฒฝ ์ด๋ ฅ + +| ๋ฒ„์ „ | ๋‚ ์งœ | ๋ณ€๊ฒฝ ๋‚ด์šฉ | ์ž‘์„ฑ์ž | +|------|------|----------|--------| +| 1.0 | 2025-10-21 | ์ตœ์ดˆ ์ž‘์„ฑ | UI/UX Designer | + +--- + +## ๋ถ€๋ก + +### A. ๋””์ž์ธ ์‹œ์Šคํ…œ (์ถ”ํ›„ ์ž‘์„ฑ ์˜ˆ์ •) +- ์ปฌ๋Ÿฌ ํŒ”๋ ˆํŠธ +- ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ ์‹œ์Šคํ…œ +- ์ŠคํŽ˜์ด์‹ฑ ์‹œ์Šคํ…œ +- ์•„์ด์ฝ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ +- ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ + +### B. ํ”„๋กœํ† ํƒ€์ž… ๋งํฌ (์ถ”ํ›„ ์ถ”๊ฐ€ ์˜ˆ์ •) +- Figma ํ”„๋กœํ† ํƒ€์ž… ๋งํฌ +- HTML ํ”„๋กœํ† ํƒ€์ž… ๋งํฌ + +### C. ์‚ฌ์šฉ์„ฑ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ (์ถ”ํ›„ ์ถ”๊ฐ€ ์˜ˆ์ •) +- ํ…Œ์ŠคํŠธ ์ฐธ๊ฐ€์ž ์ •๋ณด +- ์ฃผ์š” ๋ฐœ๊ฒฌ ์‚ฌํ•ญ +- ๊ฐœ์„  ๊ถŒ์žฅ ์‚ฌํ•ญ + +--- + +**๋ฌธ์„œ ๋** diff --git a/design/userstory-table.md b/design/userstory-table.md new file mode 100644 index 0000000..76e8930 --- /dev/null +++ b/design/userstory-table.md @@ -0,0 +1,344 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ์œ ์ €์Šคํ† ๋ฆฌ ๋ชฉ๋ก + +## ์ „์ฒด ์œ ์ €์Šคํ† ๋ฆฌ ์š”์•ฝ + +| ์„œ๋น„์Šค | ID | ์œ ์ €์Šคํ† ๋ฆฌ | ์šฐ์„ ์ˆœ์œ„ | ๋ณต์žก๋„ | +|--------|-----|-----------|----------|--------| +| **User** | UFR-USER-010 | [ํšŒ์›๊ฐ€์ž…] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ๊ฐ„ํŽธํ•˜๊ฒŒ ํšŒ์›๊ฐ€์ž…ํ•˜๊ณ  ์‹ถ๋‹ค | M | 21 | +| | UFR-USER-020 | [๋กœ๊ทธ์ธ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ๊ฐ„ํŽธํ•˜๊ฒŒ ๋กœ๊ทธ์ธํ•˜๊ณ  ์‹ถ๋‹ค | M | 8 | +| | UFR-USER-030 | [ํ”„๋กœํ•„๊ด€๋ฆฌ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ํ”„๋กœํ•„ ์ •๋ณด๋ฅผ ํŽธ์ง‘ํ•˜๊ณ  ์‹ถ๋‹ค | C | 8 | +| | UFR-USER-040 | [๋กœ๊ทธ์•„์›ƒ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์•„์›ƒํ•˜๊ณ  ์‹ถ๋‹ค | S | 3 | +| **Event** | UFR-EVENT-010 | [๋Œ€์‹œ๋ณด๋“œ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ด๋ฒคํŠธ๋ฅผ ํ•œ๋ˆˆ์— ํ™•์ธํ•˜๊ณ  ์‹ถ๋‹ค | S | 13 | +| | UFR-EVENT-020 | [์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ์ด๋ฒคํŠธ ๋ชฉ์ ์„ ๋จผ์ € ์„ ํƒํ•˜๊ณ  ์‹ถ๋‹ค | M | 5 | +| | UFR-EVENT-030 | [AI์ด๋ฒคํŠธ์ถ”์ฒœ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ์™€ ํ•จ๊ป˜ 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ์ถ”์ฒœ์„ ํ™•์ธํ•˜๊ณ  ์‹ถ๋‹ค | M | 34 | +| | UFR-EVENT-040 | [๋ฐฐํฌ์ฑ„๋„์„ ํƒ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ๋‹ค์ค‘ ๋ฐฐํฌ ์ฑ„๋„์„ ์„ ํƒํ•˜๊ณ  ์‹ถ๋‹ค | M | 13 | +| | UFR-EVENT-050 | [์ตœ์ข…์Šน์ธ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ๋ชจ๋“  ์„ค์ •์„ ์ตœ์ข… ํ™•์ธํ•˜๊ณ  ์Šน์ธํ•˜๊ณ  ์‹ถ๋‹ค | M | 13 | +| | UFR-EVENT-060 | [์ด๋ฒคํŠธ์ƒ์„ธ์กฐํšŒ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ์ด๋ฒคํŠธ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๊ณ  ์‹ถ๋‹ค | S | 13 | +| | UFR-EVENT-070 | [์ด๋ฒคํŠธ๋ชฉ๋ก๊ด€๋ฆฌ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ์ „์ฒด ์ด๋ฒคํŠธ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๊ณ  ํ•„ํ„ฐ๋ง/๊ฒ€์ƒ‰ํ•˜๊ณ  ์‹ถ๋‹ค | S | 13 | +| **AI** | UFR-AI-010 | [AIํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ด๋ฒคํŠธ์ถ”์ฒœ] AI ์‹œ์Šคํ…œ์œผ๋กœ์„œ ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ๋ฅผ ๋ถ„์„ํ•˜๊ณ  3๊ฐ€์ง€ ์ตœ์ ํ™”๋œ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ์„ ์ƒ์„ฑํ•˜๊ณ  ์‹ถ๋‹ค | M | 34 | +| **Content** | UFR-CONT-010 | [SNS์ด๋ฏธ์ง€์ƒ์„ฑ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ AI๊ฐ€ ์ž๋™์œผ๋กœ 3๊ฐ€์ง€ ์Šคํƒ€์ผ์˜ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ๋ฅผ ์›ํ•œ๋‹ค | M | 21 | +| | UFR-CONT-020 | [์ฝ˜ํ…์ธ ํŽธ์ง‘] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ„๋‹จํ•œ ํŽธ์ง‘ ๊ธฐ๋Šฅ์œผ๋กœ ์กฐ์ •ํ•˜๊ณ  ์‹ถ๋‹ค | S | 13 | +| **Distribution** | UFR-DIST-010 | [๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ์›ํด๋ฆญ์œผ๋กœ ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ๋‹ค | M | 21 | +| | UFR-DIST-020 | [๋ฐฐํฌ์ƒํƒœ๋ชจ๋‹ˆํ„ฐ๋ง] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ๋ฐฐํฌ ์ƒํƒœ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ์‹ถ๋‹ค | S | 13 | +| **Participation** | UFR-PART-010 | [์ด๋ฒคํŠธ์ฐธ์—ฌ] ๊ณ ๊ฐ์œผ๋กœ์„œ ์ตœ์†Œํ•œ์˜ ์ •๋ณด๋งŒ ์ž…๋ ฅํ•˜๊ณ  ์ด๋ฒคํŠธ์— ์ฐธ์—ฌํ•˜๊ณ  ์‹ถ๋‹ค | M | 13 | +| | UFR-PART-020 | [์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ์ฐธ์—ฌ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๊ณ  ์‹ถ๋‹ค | S | 8 | +| | UFR-PART-030 | [๋‹น์ฒจ์ž์ถ”์ฒจ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ์ž๋™ ์ถ”์ฒจ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค | M | 13 | +| **Analytics** | UFR-ANAL-010 | [์‹ค์‹œ๊ฐ„ํ†ตํ•ฉ๋Œ€์‹œ๋ณด๋“œ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ ๋ชจ๋“  ์ง€ํ‘œ๊ฐ€ ํ†ตํ•ฉ๋œ ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋ณด๊ณ  ์‹ถ๋‹ค | M | 34 | + +--- + +## ์šฐ์„ ์ˆœ์œ„๋ณ„ ํ†ต๊ณ„ + +| ์šฐ์„ ์ˆœ์œ„ | ๊ฐœ์ˆ˜ | ๋น„์œจ | +|---------|------|------| +| M (ํ•„์ˆ˜) | 13 | 65.0% | +| S (์„ ํƒ) | 6 | 30.0% | +| C (์กฐ๊ฑด๋ถ€) | 1 | 5.0% | +| **์ด๊ณ„** | **20** | **100%** | + +--- + +## ์„œ๋น„์Šค๋ณ„ ํ†ต๊ณ„ + +| ์„œ๋น„์Šค | ์œ ์ €์Šคํ† ๋ฆฌ ์ˆ˜ | ํ‰๊ท  ๋ณต์žก๋„ | ํ•„์ˆ˜(M) | ์„ ํƒ(S) | ์กฐ๊ฑด๋ถ€(C) | +|--------|-------------|-----------|---------|---------|----------| +| User | 4 | 10.0 | 2 | 1 | 1 | +| Event | 7 | 14.9 | 4 | 3 | 0 | +| AI | 1 | 34.0 | 1 | 0 | 0 | +| Content | 2 | 17.0 | 1 | 1 | 0 | +| Distribution | 2 | 17.0 | 1 | 1 | 0 | +| Participation | 3 | 11.3 | 2 | 1 | 0 | +| Analytics | 1 | 34.0 | 1 | 0 | 0 | +| **์ด๊ณ„** | **20** | **16.9** | **12** | **7** | **1** | + +--- + +## ๋ณต์žก๋„๋ณ„ ํ†ต๊ณ„ + +| ๋ณต์žก๋„ ๋ฒ”์œ„ | ๊ฐœ์ˆ˜ | ์œ ์ €์Šคํ† ๋ฆฌ | +|-----------|------|-----------| +| 1-5 (๋‚ฎ์Œ) | 2 | UFR-USER-040, UFR-EVENT-020 | +| 6-13 (์ค‘๊ฐ„) | 11 | UFR-USER-020, UFR-USER-030, UFR-EVENT-010, UFR-EVENT-040, UFR-EVENT-050, UFR-EVENT-060, UFR-EVENT-070, UFR-CONT-020, UFR-DIST-020, UFR-PART-010, UFR-PART-020, UFR-PART-030 | +| 14-21 (๋†’์Œ) | 4 | UFR-USER-010, UFR-CONT-010, UFR-DIST-010 | +| 22+ (๋งค์šฐ ๋†’์Œ) | 3 | UFR-EVENT-030, UFR-AI-010, UFR-ANAL-010 | + +--- + +## ์ฃผ์š” ๊ธฐ๋Šฅ๋ณ„ ์ƒ์„ธ ๋ชฉ๋ก + +### 1. User ์„œ๋น„์Šค (ํšŒ์› ์ธ์ฆ ๋ฐ ๋งค์žฅ ์ •๋ณด ๊ด€๋ฆฌ) + +| ID | ๊ธฐ๋Šฅ | ์šฐ์„ ์ˆœ์œ„ | ๋ณต์žก๋„ | ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ | +|----|------|----------|--------|-------------| +| UFR-USER-010 | ํšŒ์›๊ฐ€์ž… | M | 21 | ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ, ๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ, ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ | +| UFR-USER-020 | ๋กœ๊ทธ์ธ | M | 8 | ์ „ํ™”๋ฒˆํ˜ธ/๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ, ๋กœ๊ทธ์ธ ์œ ์ง€ ์˜ต์…˜, ๋Œ€์‹œ๋ณด๋“œ ์ด๋™ | +| UFR-USER-030 | ํ”„๋กœํ•„๊ด€๋ฆฌ | C | 8 | ๊ธฐ๋ณธ ์ •๋ณด, ๋งค์žฅ ์ •๋ณด, ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ | +| UFR-USER-040 | ๋กœ๊ทธ์•„์›ƒ | S | 3 | ์•ˆ์ „ํ•œ ์„ธ์…˜ ์ข…๋ฃŒ, ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ | + +**๊ฒ€์ฆ ๋กœ์ง:** +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ํ˜•์‹ ๊ฒ€์ฆ (XXX-XX-XXXXX) +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ํ™•์ธ ๋ฐ ํœดํ์—… ์—ฌ๋ถ€ ํ™•์ธ (๊ตญ์„ธ์ฒญ API) +- ๋งค์žฅ๋ช…๊ณผ ์‚ฌ์—…์ž ์ •๋ณด ์ผ์น˜ ํ™•์ธ + +**์ฃผ์š” ํ๋ฆ„:** +1. UFR-USER-020 (๋กœ๊ทธ์ธ) โ†’ UFR-EVENT-010 (๋Œ€์‹œ๋ณด๋“œ) +2. ์‹ ๊ทœ ์‚ฌ์šฉ์ž: ํšŒ์›๊ฐ€์ž…(UFR-USER-010) โ†’ ๋Œ€์‹œ๋ณด๋“œ(UFR-EVENT-010) +3. ๋Œ€์‹œ๋ณด๋“œ์—์„œ "์ƒˆ ์ด๋ฒคํŠธ ๋งŒ๋“ค๊ธฐ" โ†’ UFR-EVENT-020 + +--- + +### 2. Event ์„œ๋น„์Šค (์ด๋ฒคํŠธ ๊ด€๋ฆฌ) + +| ID | ๊ธฐ๋Šฅ | ์šฐ์„ ์ˆœ์œ„ | ๋ณต์žก๋„ | ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ | +|----|------|----------|--------|-------------| +| UFR-EVENT-010 | ๋Œ€์‹œ๋ณด๋“œ | S | 13 | ์ง„ํ–‰์ค‘/์˜ˆ์ •/์ข…๋ฃŒ ์ด๋ฒคํŠธ ๋ชฉ๋ก, ํ†ต๊ณ„, ์ƒˆ ์ด๋ฒคํŠธ ๋งŒ๋“ค๊ธฐ ๋ฒ„ํŠผ | +| UFR-EVENT-020 | ์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ | M | 5 | ์‹ ๊ทœ๊ณ ๊ฐ ์œ ์น˜/์žฌ๋ฐฉ๋ฌธ ์œ ๋„/๋งค์ถœ ์ฆ๋Œ€/์ธ์ง€๋„ ํ–ฅ์ƒ ์„ ํƒ | +| UFR-EVENT-030 | AI์ด๋ฒคํŠธ์ถ”์ฒœ | M | 34 | ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ, 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ์ถ”์ฒœ, ์ œ๋ชฉ/๊ฒฝํ’ˆ ๊ฐ„๋‹จ ์ˆ˜์ • | +| UFR-EVENT-040 | ๋ฐฐํฌ์ฑ„๋„์„ ํƒ | M | 13 | ์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS ์„ ํƒ, ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜ | +| UFR-EVENT-050 | ์ตœ์ข…์Šน์ธ | M | 13 | ์ด๋ฒคํŠธ ์ •๋ณด ํ™•์ธ, ์Šน์ธ ์ฒ˜๋ฆฌ, ๋ฐฐํฌ ์‹œ์ž‘ | +| UFR-EVENT-060 | ์ด๋ฒคํŠธ์ƒ์„ธ์กฐํšŒ | S | 13 | ๊ธฐ๋ณธ ์ •๋ณด, ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„, ๋ฐฐํฌ ํ˜„ํ™ฉ, ์ฐธ์—ฌ์ž ๋ชฉ๋ก | +| UFR-EVENT-070 | ์ด๋ฒคํŠธ๋ชฉ๋ก๊ด€๋ฆฌ | S | 13 | ์ „์ฒด ์ด๋ฒคํŠธ ๋ชฉ๋ก, ํ•„ํ„ฐ๋ง, ๊ฒ€์ƒ‰, ์ •๋ ฌ | + +**์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ:** +1. ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ (UFR-EVENT-020) +2. AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ (UFR-EVENT-030) +3. ์ฝ˜ํ…์ธ  ์ƒ์„ฑ (UFR-CONT-010) +4. ๋ฐฐํฌ์ฑ„๋„ ์„ ํƒ (UFR-EVENT-040) +5. ์ตœ์ข…์Šน์ธ (UFR-EVENT-050) + +--- + +### 3. AI ์„œ๋น„์Šค (AI ๋ถ„์„ ๋ฐ ์ถ”์ฒœ) + +| ID | ๊ธฐ๋Šฅ | ์šฐ์„ ์ˆœ์œ„ | ๋ณต์žก๋„ | ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ | AI ๋ชจ๋ธ | +|----|------|----------|--------|-------------|---------| +| UFR-AI-010 | AIํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ด๋ฒคํŠธ์ถ”์ฒœ | M | 34 | ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ ๋ถ„์„, 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ ์ƒ์„ฑ, 10์ดˆ ์ด๋‚ด ์™„๋ฃŒ | Claude API / GPT-4 API | + +**๋ถ„์„ ํ•ญ๋ชฉ:** +- ์—…์ข… ํŠธ๋ Œ๋“œ: ์ตœ๊ทผ ์„ฑ๊ณต ์ด๋ฒคํŠธ, ๊ณ ๊ฐ ์„ ํ˜ธ ๊ฒฝํ’ˆ, ํšจ๊ณผ์ ์ธ ์ฐธ์—ฌ ๋ฐฉ๋ฒ• +- ์ง€์—ญ ํŠน์„ฑ: ์ง€์—ญ๋ณ„ ์ด๋ฒคํŠธ ์„ฑ๊ณต๋ฅ , ๊ณ ๊ฐ ํŠน์„ฑ +- ์‹œ์ฆŒ ํŠน์„ฑ: ๊ณ„์ ˆ/์‹œ๊ธฐ๋ณ„ ์ถ”์ฒœ ์ด๋ฒคํŠธ + +**์ถ”์ฒœ ๊ธฐ์ค€:** +- ์˜ต์…˜ 1: ์ €๋น„์šฉ, ๋†’์€ ์ฐธ์—ฌ์œจ ์ค‘์‹ฌ +- ์˜ต์…˜ 2: ์ค‘๋น„์šฉ, ๊ท ํ˜•์žกํžŒ ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  +- ์˜ต์…˜ 3: ๊ณ ๋น„์šฉ, ๋†’์€ ๋งค์ถœ ์ฆ๋Œ€ ํšจ๊ณผ + +**์„ฑ๋Šฅ ๋ชฉํ‘œ:** ์ „์ฒด ๊ธฐํš ๊ณผ์ • 10์ดˆ ์ด๋‚ด ์™„๋ฃŒ (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) + +--- + +### 4. Content ์„œ๋น„์Šค (์ฝ˜ํ…์ธ  ์ƒ์„ฑ) + +| ID | ๊ธฐ๋Šฅ | ์šฐ์„ ์ˆœ์œ„ | ๋ณต์žก๋„ | ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ | AI ๋ชจ๋ธ | +|----|------|----------|--------|-------------|---------| +| UFR-CONT-010 | SNS์ด๋ฏธ์ง€์ƒ์„ฑ | M | 21 | 3๊ฐ€์ง€ ์Šคํƒ€์ผ (์‹ฌํ”Œ/ํ™”๋ ค/ํŠธ๋ Œ๋””), ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™” | Stable Diffusion / DALL-E | +| UFR-CONT-020 | ์ฝ˜ํ…์ธ ํŽธ์ง‘ | S | 13 | ํ…์ŠคํŠธ ์ˆ˜์ •, ์ƒ‰์ƒ ์กฐ์ •, ๋กœ๊ณ  ์œ„์น˜ ๋ณ€๊ฒฝ | - | + +**ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™”:** +- Instagram: 1080x1080 +- Naver Blog: 800x600 +- Kakao Channel: 800x800 + +**์„ฑ๋Šฅ ๋ชฉํ‘œ:** +- ์ด๋ฏธ์ง€ ์ƒ์„ฑ: 30์ดˆ ์ด๋‚ด +- ์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ œ๊ณต + +--- + +### 5. Distribution ์„œ๋น„์Šค (๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ) + +| ID | ๊ธฐ๋Šฅ | ์šฐ์„ ์ˆœ์œ„ | ๋ณต์žก๋„ | ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ | ์—ฐ๋™ API | +|----|------|----------|--------|-------------|---------| +| UFR-DIST-010 | ๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ | M | 21 | ์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS ๋™์‹œ ๋ฐฐํฌ, 1๋ถ„ ์ด๋‚ด ์™„๋ฃŒ | ์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS APIs | +| UFR-DIST-020 | ๋ฐฐํฌ์ƒํƒœ๋ชจ๋‹ˆํ„ฐ๋ง | S | 13 | ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ์ƒํƒœ, ์ง„ํ–‰๋ฅ , ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€, ์žฌ์‹œ๋„ ๊ธฐ๋Šฅ | - | + +**๋ฐฐํฌ ์ฑ„๋„:** +1. ์šฐ๋ฆฌ๋™๋„คTV (๋ฐ˜๊ฒฝ 500m/1km, ์†ก์ถœ ์‹œ๊ฐ„๋Œ€) +2. ๋ง๊ณ ๋น„์ฆˆ (๋งค์žฅ ์ „ํ™” ์—ฐ๊ฒฐ์Œ ์—…๋ฐ์ดํŠธ) +3. ์ง€๋‹ˆTV (ํƒ€๊ฒŸ ์ง€์—ญ, ๋…ธ์ถœ ์‹œ๊ฐ„๋Œ€, ์˜ˆ์‚ฐ) +4. SNS (Instagram, Naver Blog, Kakao Channel) + +**์„ฑ๋Šฅ ๋ชฉํ‘œ:** ์ „์ฒด ๋ฐฐํฌ ๊ณผ์ • 1๋ถ„ ์ด๋‚ด ์™„๋ฃŒ + +--- + +### 6. Participation ์„œ๋น„์Šค (์ด๋ฒคํŠธ ์ฐธ์—ฌ ๋ฐ ์ ‘์ˆ˜ ๊ด€๋ฆฌ) + +| ID | ๊ธฐ๋Šฅ | ์šฐ์„ ์ˆœ์œ„ | ๋ณต์žก๋„ | ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ | +|----|------|----------|--------|-------------| +| UFR-PART-010 | ์ด๋ฒคํŠธ์ฐธ์—ฌ | M | 13 | ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ ์ž…๋ ฅ, ์ฐธ์—ฌ ๊ฒฝ๋กœ ์ถ”์ , ์‘๋ชจ๋ฒˆํ˜ธ ๋ฐœ๊ธ‰, ์ค‘๋ณต ๋ฐฉ์ง€ | +| UFR-PART-020 | ์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ | S | 8 | ์ฐธ์—ฌ์ž ํ…Œ์ด๋ธ”, ํ•„ํ„ฐ๋ง, ๊ฒ€์ƒ‰, ๊ฐœ์ธ์ •๋ณด ๋งˆ์Šคํ‚น | +| UFR-PART-030 | ๋‹น์ฒจ์ž์ถ”์ฒจ | M | 13 | ์ž๋™ ์ถ”์ฒจ, ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์ , ์žฌ์ถ”์ฒจ ๊ธฐ๋Šฅ | + +**์ •์ฑ…:** +- 1์ธ 1ํšŒ ์ฐธ์—ฌ ์ œํ•œ (์ „ํ™”๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜) +- ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ ๊ทœ์ • ์ค€์ˆ˜ +- ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์  ๋ถ€์—ฌ (์„ ํƒ ๊ฐ€๋Šฅ) + +--- + +### 7. Analytics ์„œ๋น„์Šค (์‹ค์‹œ๊ฐ„ ํšจ๊ณผ ์ธก์ • ๋ฐ ๋ถ„์„) + +| ID | ๊ธฐ๋Šฅ | ์šฐ์„ ์ˆœ์œ„ | ๋ณต์žก๋„ | ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ | ๋ฐ์ดํ„ฐ ์†Œ์Šค | +|----|------|----------|--------|-------------|------------| +| UFR-ANAL-010 | ์‹ค์‹œ๊ฐ„ํ†ตํ•ฉ๋Œ€์‹œ๋ณด๋“œ | M | 34 | ์ฐธ์—ฌ์ž ์ˆ˜, ๋…ธ์ถœ ์ˆ˜, ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ , ๋งค์ถœ ์ฆ๊ฐ€์œจ, ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ, 5๋ถ„ ๊ฐ„๊ฒฉ ์—…๋ฐ์ดํŠธ | KT ์ฑ„๋„ API, POS, SNS API | + +**๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์„ฑ:** +1. ์ƒ๋‹จ ์š”์•ฝ ์นด๋“œ: ์ด ์ฐธ์—ฌ์ž ์ˆ˜, ์ด ๋…ธ์ถœ ์ˆ˜, ์˜ˆ์ƒ ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ , ๋งค์ถœ ์ฆ๊ฐ€์œจ +2. ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„: ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„, ์› ๊ทธ๋ž˜ํ”„ +3. ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด: ๋ผ์ธ ์ฐจํŠธ +4. ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  ์ƒ์„ธ ๋ถ„์„: ์ด ๋น„์šฉ, ์˜ˆ์ƒ ์ˆ˜์ต, ์†์ต๋ถ„๊ธฐ์  +5. ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ถ„์„: ์—ฐ๋ น๋Œ€๋ณ„, ์„ฑ๋ณ„, ์ง€์—ญ๋ณ„ ๋ถ„ํฌ +6. ๋น„๊ต ๋ถ„์„: ์—…์ข… ํ‰๊ท , ์ด์ „ ์ด๋ฒคํŠธ ๋Œ€๋น„ + +**๋ฐ์ดํ„ฐ ์ˆ˜์ง‘:** +- ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ (5๋ถ„ ๊ฐ„๊ฒฉ) +- WebSocket ๋˜๋Š” SSE๋ฅผ ํ†ตํ•œ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ +- ๋ฐ์ดํ„ฐ ์บ์‹ฑ (Redis) + +--- + +## ๊ธฐ์ˆ  ์Šคํƒ ์š”์•ฝ + +### AI/ML ๋ชจ๋ธ +| ๋ชจ๋ธ | ์šฉ๋„ | ๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ | +|------|------|----------------| +| Claude API / GPT-4 API | ํŠธ๋ Œ๋“œ ๋ถ„์„, ์ด๋ฒคํŠธ ์ถ”์ฒœ, ํ™๋ณด ๋ฌธ๊ตฌ ์ƒ์„ฑ | UFR-AI-010 | +| Stable Diffusion / DALL-E | ์ด๋ฏธ์ง€ ์ƒ์„ฑ | UFR-CONT-010 | + +### ์™ธ๋ถ€ API ์—ฐ๋™ +| API | ์šฉ๋„ | ๊ด€๋ จ ์œ ์ €์Šคํ† ๋ฆฌ | +|-----|------|----------------| +| ๊ตญ์„ธ์ฒญ API | ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ | UFR-USER-010 | +| ์šฐ๋ฆฌ๋™๋„คTV API | ์ง€์—ญ ํƒ€๊ฒŸํŒ… ์˜์ƒ ์†ก์ถœ | UFR-DIST-010 | +| ๋ง๊ณ ๋น„์ฆˆ API | ์—ฐ๊ฒฐ์Œ ์—…๋ฐ์ดํŠธ | UFR-DIST-010 | +| ์ง€๋‹ˆTV API | TV ๊ด‘๊ณ  ๋ฐฐํฌ | UFR-DIST-010 | +| Instagram API | SNS ์ž๋™ ํฌ์ŠคํŒ…, ์„ฑ๊ณผ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ | UFR-DIST-010, UFR-ANAL-010 | +| Naver API | ๋ธ”๋กœ๊ทธ ์ž๋™ ํฌ์ŠคํŒ… | UFR-DIST-010 | +| Kakao API | ์นด์นด์˜ค ์ฑ„๋„ ์ž๋™ ํฌ์ŠคํŒ…, ์„ฑ๊ณผ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ | UFR-DIST-010, UFR-ANAL-010 | + +--- + +## ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ API Gateway โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ User Service โ”‚ โ”‚ Event Service โ”‚ โ”‚ AI Service โ”‚ +โ”‚ (UFR-USER) โ”‚ โ”‚ (UFR-EVENT) โ”‚ โ”‚ (UFR-AI) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Content โ”‚ โ”‚ Distribution โ”‚ โ”‚ Participation โ”‚ +โ”‚ (UFR-CONT) โ”‚ โ”‚ (UFR-DIST) โ”‚ โ”‚ (UFR-PART) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Analytics โ”‚ + โ”‚ (UFR-ANAL) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ์„œ๋น„์Šค ๊ฐ„ ์˜์กด์„ฑ + +- User โ†’ Event: ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด ์ œ๊ณต +- Event โ†’ AI: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ ์š”์ฒญ +- Event โ†’ Content: ์ฝ˜ํ…์ธ  ์ƒ์„ฑ ์š”์ฒญ +- Event โ†’ Distribution: ๋ฐฐํฌ ์š”์ฒญ +- Distribution โ†’ Content: ์ƒ์„ฑ๋œ ์ฝ˜ํ…์ธ  ์‚ฌ์šฉ +- Participation โ†’ Analytics: ์ฐธ์—ฌ์ž ๋ฐ์ดํ„ฐ ์ œ๊ณต +- Distribution โ†’ Analytics: ๋…ธ์ถœ ๋ฐ์ดํ„ฐ ์ œ๊ณต + +--- + +## ๊ฐœ๋ฐœ ์šฐ์„ ์ˆœ์œ„ ๋กœ๋“œ๋งต + +### Phase 1: MVP (8์ฃผ) +**๋ชฉํ‘œ:** ๊ธฐ๋ณธ ์ด๋ฒคํŠธ ๊ธฐํš ๋ฐ ๋ฐฐํฌ ๊ธฐ๋Šฅ + +| ์ˆœ์„œ | ์„œ๋น„์Šค | ์œ ์ €์Šคํ† ๋ฆฌ | ๋น„๊ณ  | +|------|--------|-----------|------| +| 1 | User | UFR-USER-010, UFR-USER-020 | ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ | +| 2 | Event | UFR-EVENT-020, UFR-EVENT-030, UFR-EVENT-050 | ๋ชฉ์  ์„ ํƒ, AI ์ถ”์ฒœ, ์Šน์ธ | +| 3 | AI | UFR-AI-010 | ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ | +| 4 | Content | UFR-CONT-010 | ์ด๋ฏธ์ง€ ์ƒ์„ฑ | +| 5 | Distribution | UFR-DIST-010 | ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ | +| 6 | Participation | UFR-PART-010, UFR-PART-030 | ์ฐธ์—ฌ ์‹ ์ฒญ, ์ž๋™ ์ถ”์ฒจ | + +### Phase 2: ๊ณ ๋„ํ™” (6์ฃผ) +**๋ชฉํ‘œ:** ๊ด€๋ฆฌ ๋ฐ ๋ถ„์„ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + +| ์ˆœ์„œ | ์„œ๋น„์Šค | ์œ ์ €์Šคํ† ๋ฆฌ | ๋น„๊ณ  | +|------|--------|-----------|------| +| 7 | Event | UFR-EVENT-010, UFR-EVENT-040, UFR-EVENT-060, UFR-EVENT-070 | ๋Œ€์‹œ๋ณด๋“œ, ์ฑ„๋„ ์„ ํƒ, ์ƒ์„ธ ์กฐํšŒ, ๋ชฉ๋ก ๊ด€๋ฆฌ | +| 8 | Analytics | UFR-ANAL-010 | ์‹ค์‹œ๊ฐ„ ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ | +| 9 | Distribution | UFR-DIST-020 | ๋ฐฐํฌ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง | +| 10 | Participation | UFR-PART-020 | ์ฐธ์—ฌ์ž ๋ชฉ๋ก ์กฐํšŒ | + +### Phase 3: ์™„์„ฑ (2์ฃผ) +**๋ชฉํ‘œ:** ์„ ํƒ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + +| ์ˆœ์„œ | ์„œ๋น„์Šค | ์œ ์ €์Šคํ† ๋ฆฌ | ๋น„๊ณ  | +|------|--------|-----------|------| +| 11 | User | UFR-USER-030, UFR-USER-040 | ํ”„๋กœํ•„ ๊ด€๋ฆฌ, ๋กœ๊ทธ์•„์›ƒ | +| 12 | Content | UFR-CONT-020 | ์ฝ˜ํ…์ธ  ํŽธ์ง‘ | + +--- + +## ์ฃผ์š” ๊ธฐ์ˆ ์  ๋ฆฌ์Šคํฌ ๋ฐ ํ•ด๊ฒฐ๋ฐฉ์•ˆ + +### 1. AI ์‘๋‹ต ์‹œ๊ฐ„ (10์ดˆ ๋ชฉํ‘œ) +- **๋ฆฌ์Šคํฌ**: AI API ์‘๋‹ต์ด ๋ชฉํ‘œ ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ•  ์ˆ˜ ์žˆ์Œ +- **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: + - ํ”„๋กฌํ”„ํŠธ ์ตœ์ ํ™”๋กœ ์‘๋‹ต ์‹œ๊ฐ„ ๋‹จ์ถ• + - ํŠธ๋ Œ๋“œ ๋ถ„์„๊ณผ ์ด๋ฒคํŠธ ์ถ”์ฒœ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ + - ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ + ํด๋ง ๋ฐฉ์‹ + - ๋ถ€๋ถ„ ์‘๋‹ต ์ŠคํŠธ๋ฆฌ๋ฐ + +### 2. ๋‹ค์ค‘ ์™ธ๋ถ€ API ์˜์กด์„ฑ +- **๋ฆฌ์Šคํฌ**: API ์žฅ์•  ์‹œ ์„œ๋น„์Šค ์ค‘๋‹จ +- **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: + - ๊ฐ API๋ณ„ ํด๋ฐฑ ์ „๋žต ์ˆ˜๋ฆฝ + - ์บ์‹ฑ ์ ๊ทน ํ™œ์šฉ + - ์จํ‚ท ๋ธŒ๋ ˆ์ด์ปค ํŒจํ„ด ์ ์šฉ + - ์ฑ„๋„๋ณ„ ๋…๋ฆฝ ์ฒ˜๋ฆฌ + +### 3. ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ์„ฑ๋Šฅ +- **๋ฆฌ์Šคํฌ**: ๋‹ค์ค‘ ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ†ตํ•ฉ์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์ €ํ•˜ +- **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: + - Redis ์บ์‹ฑ + - 5๋ถ„ ๊ฐ„๊ฒฉ ๋ฐ์ดํ„ฐ ํด๋ง + - WebSocket/SSE๋ฅผ ํ†ตํ•œ ํšจ์œจ์ ์ธ ์—…๋ฐ์ดํŠธ + - ์ฐจํŠธ ๋ฐ์ดํ„ฐ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ + +### 4. ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋น„์šฉ ๋ฐ ์‹œ๊ฐ„ +- **๋ฆฌ์Šคํฌ**: ์ด๋ฏธ์ง€ ์ƒ์„ฑ API ๋น„์šฉ ๋ฐ ์ƒ์„ฑ ์‹œ๊ฐ„ +- **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: + - ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ์บ์‹ฑ + - CDN ํ™œ์šฉ + - ๋น„๋™๊ธฐ ์ƒ์„ฑ + ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ + +--- + +## ์˜ˆ์ƒ ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„ + +- User ์„œ๋น„์Šค: 2์ฃผ +- Event ์„œ๋น„์Šค: 3์ฃผ +- AI ์„œ๋น„์Šค: 4์ฃผ (ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง ํฌํ•จ) +- Content ์„œ๋น„์Šค: 3์ฃผ +- Distribution ์„œ๋น„์Šค: 4์ฃผ (๋‹ค์ค‘ API ์—ฐ๋™) +- Participation ์„œ๋น„์Šค: 2์ฃผ +- Analytics ์„œ๋น„์Šค: 4์ฃผ (ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ) + +**์ด ์˜ˆ์ƒ ๊ธฐ๊ฐ„**: 12-16์ฃผ (๋ณ‘๋ ฌ ๊ฐœ๋ฐœ ์‹œ) diff --git a/design/userstory.md b/design/userstory.md new file mode 100644 index 0000000..1528798 --- /dev/null +++ b/design/userstory.md @@ -0,0 +1,998 @@ +# KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ์œ ์ €์Šคํ† ๋ฆฌ ver2.0 + +- [KT AI ๊ธฐ๋ฐ˜ ์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ ์ž๋™ ์ƒ์„ฑ ์„œ๋น„์Šค - ์œ ์ €์Šคํ† ๋ฆฌ ver2.0](#kt-ai-๊ธฐ๋ฐ˜-์†Œ์ƒ๊ณต์ธ-์ด๋ฒคํŠธ-์ž๋™-์ƒ์„ฑ-์„œ๋น„์Šค---์œ ์ €์Šคํ† ๋ฆฌ-ver20) + - [๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ตฌ์„ฑ](#๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค-๊ตฌ์„ฑ) + - [์œ ์ €์Šคํ† ๋ฆฌ](#์œ ์ €์Šคํ† ๋ฆฌ) + - [๊ธฐ์ˆ  ๊ฒ€ํ†  ๊ฒฐ๊ณผ](#๊ธฐ์ˆ -๊ฒ€ํ† -๊ฒฐ๊ณผ) + +--- + +## ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ตฌ์„ฑ +1. **User** - ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๋งค์žฅ์ •๋ณด ๊ด€๋ฆฌ +2. **Event** - ์ด๋ฒคํŠธ ๊ธฐํš ๋ฐ ๊ด€๋ฆฌ +3. **AI** - AI ๊ธฐ๋ฐ˜ ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ +4. **Content** - SNS ์ฝ˜ํ…์ธ  ์ƒ์„ฑ +5. **Distribution** - ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ ๊ด€๋ฆฌ +6. **Participation** - ์ด๋ฒคํŠธ ์ฐธ์—ฌ ๋ฐ ๋‹น์ฒจ์ž ๊ด€๋ฆฌ +7. **Analytics** - ์‹ค์‹œ๊ฐ„ ํšจ๊ณผ ์ธก์ • ๋ฐ ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ + +--- + +## ์œ ์ €์Šคํ† ๋ฆฌ +``` +1. User ์„œ๋น„์Šค +1) ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๋งค์žฅ์ •๋ณด ๊ด€๋ฆฌ +UFR-USER-010: [ํšŒ์›๊ฐ€์ž…] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด | ๊ฐ„ํŽธํ•˜๊ฒŒ ํšŒ์›๊ฐ€์ž…ํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ์‹ ๊ทœ ํšŒ์›๊ฐ€์ž… + ๋ฏธ๋กœ๊ทธ์ธ ์ƒํƒœ์—์„œ ํšŒ์›๊ฐ€์ž… ํ™”๋ฉด์— ์ ‘๊ทผํ•œ ์ƒํ™ฉ์—์„œ | ํ•„์ˆ˜ ์ •๋ณด๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•˜๊ณ  ํšŒ์›๊ฐ€์ž… ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด | ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ์„ ๊ฑฐ์ณ ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋œ๋‹ค. + + [์ž…๋ ฅ ์š”๊ตฌ์‚ฌํ•ญ] + - ๊ธฐ๋ณธ ์ •๋ณด + - ์ด๋ฆ„: 2์ž ์ด์ƒ (ํ•œ๊ธ€/์˜๋ฌธ) + - ์ „ํ™”๋ฒˆํ˜ธ: ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ ํ˜•์‹ + - ์ด๋ฉ”์ผ: ์ด๋ฉ”์ผ ํ˜•์‹ ์ค€์ˆ˜ + - ๋น„๋ฐ€๋ฒˆํ˜ธ: 8์ž ์ด์ƒ (์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ) + + [๋งค์žฅ ์ •๋ณด ์ž…๋ ฅ] + - ๋งค์žฅ๋ช…: ํ•„์ˆ˜ + - ์—…์ข…: ์„ ํƒ (์˜ˆ: ์Œ์‹์ , ์นดํŽ˜, ์†Œ๋งค์  ๋“ฑ) + - ์ฃผ์†Œ: ํ•„์ˆ˜ + - ์˜์—…์‹œ๊ฐ„: ์„ ํƒ + - ์‚ฌ์—…์ž๋ฒˆํ˜ธ: ํ•„์ˆ˜ + - ๊ตญ์„ธ์ฒญ DB ์กฐํšŒ๋ฅผ ํ†ตํ•œ ๊ฒ€์ฆ + - ํœดํ์—… ์—ฌ๋ถ€ ํ™•์ธ + - ๊ฒ€์ฆ ์‹คํŒจ ์‹œ ์žฌ์ž…๋ ฅ ์š”์ฒญ + + [์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ] + - ์„ฑ๊ณต: "ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ โ†’ ๋Œ€์‹œ๋ณด๋“œ ํ™”๋ฉด + - ์‹คํŒจ: ๊ตฌ์ฒด์ ์ธ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ + +- M/21 + +--- + +UFR-USER-020: [๋กœ๊ทธ์ธ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด | ๊ฐ„ํŽธํ•˜๊ฒŒ ๋กœ๊ทธ์ธํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ + ๋ฏธ๋กœ๊ทธ์ธ ์ƒํƒœ์—์„œ ๋กœ๊ทธ์ธ ํ™”๋ฉด์— ์ ‘๊ทผํ•œ ์ƒํ™ฉ์—์„œ | ์ „ํ™”๋ฒˆํ˜ธ์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด | ์ธ์ฆ์ด ์™„๋ฃŒ๋˜๊ณ  ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ํ•œ๋‹ค. + + [์ž…๋ ฅ ์š”๊ตฌ์‚ฌํ•ญ] + - ์ „ํ™”๋ฒˆํ˜ธ: ๋“ฑ๋ก๋œ ์ „ํ™”๋ฒˆํ˜ธ ์ž…๋ ฅ + - ๋น„๋ฐ€๋ฒˆํ˜ธ: ํ•ด๋‹น ๊ณ„์ •์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ + - ๋กœ๊ทธ์ธ ์œ ์ง€: ์„ ํƒ ์˜ต์…˜ ์ œ๊ณต + + [์ธ์ฆ ์ฒ˜๋ฆฌ] + - ์„ฑ๊ณต: ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ฆ‰์‹œ ์ด๋™ + - ์‹คํŒจ: "์ „ํ™”๋ฒˆํ˜ธ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”" ๋ฉ”์‹œ์ง€ + +- M/8 + +--- + +UFR-USER-030: [ํ”„๋กœํ•„๊ด€๋ฆฌ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ๋‚ด ์ •๋ณด๋ฅผ ์ตœ์‹  ์ƒํƒœ๋กœ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด | ํ”„๋กœํ•„ ์ •๋ณด๋ฅผ ํŽธ์ง‘ํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ํ”„๋กœํ•„ ์ •๋ณด ์ˆ˜์ • + Bottom Navigation์˜ "ํ”„๋กœํ•„" ํƒญ์„ ํด๋ฆญํ•˜์—ฌ ํ”„๋กœํ•„ ํŽธ์ง‘ ํ™”๋ฉด์— ์ ‘๊ทผํ•œ ์ƒํ™ฉ์—์„œ | ๋ณ€๊ฒฝํ•  ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•˜๊ณ  ์ €์žฅ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด | ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์ €์žฅ๋˜๊ณ  ํ™•์ธ ๋ฉ”์‹œ์ง€๊ฐ€ ํ‘œ์‹œ๋œ๋‹ค. + + [์ˆ˜์ • ๊ฐ€๋Šฅ ํ•ญ๋ชฉ] + - ๊ธฐ๋ณธ ์ •๋ณด + - ์ด๋ฆ„: ์‹ค๋ช… ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ + - ์ „ํ™”๋ฒˆํ˜ธ: ๋ณ€๊ฒฝ ์‹œ ์žฌ์ธ์ฆ ํ•„์š” + - ์ด๋ฉ”์ผ: ์ด๋ฉ”์ผ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ + - ๋งค์žฅ ์ •๋ณด + - ๋งค์žฅ๋ช… + - ์—…์ข… + - ์ฃผ์†Œ + - ์˜์—…์‹œ๊ฐ„ + - ๋น„๋ฐ€๋ฒˆํ˜ธ + - ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ํ•„์ˆ˜ + - ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ทœ์น™: 8์ž ์ด์ƒ (์˜๋ฌธ/์ˆซ์ž/ํŠน์ˆ˜๋ฌธ์ž ํฌํ•จ) + + [์ €์žฅ ์ฒ˜๋ฆฌ] + - ์ €์žฅ ์ „ ํ™•์ธ: "๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ €์žฅํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + - ์ €์žฅ ์™„๋ฃŒ: "ํ”„๋กœํ•„์ด ์„ฑ๊ณต์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + - ์ทจ์†Œ ์„ ํƒ: "๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์ €์žฅ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ณ„์†ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + +- C/8 + +--- + +UFR-USER-040: [๋กœ๊ทธ์•„์›ƒ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ๋ณด์•ˆ์„ ์œ„ํ•ด | ์„œ๋น„์Šค ์‚ฌ์šฉ ํ›„ ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์•„์›ƒํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ์•ˆ์ „ํ•œ ๋กœ๊ทธ์•„์›ƒ + Bottom Navigation์˜ "ํ”„๋กœํ•„" ํƒญ์—์„œ ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ์„ ์„ ํƒํ•œ ์ƒํ™ฉ์—์„œ | ๋กœ๊ทธ์•„์›ƒ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ์—์„œ ํ™•์ธ์„ ํด๋ฆญํ•˜๋ฉด | ์„ธ์…˜์ด ์ข…๋ฃŒ๋˜๊ณ  ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•œ๋‹ค. + + [๋กœ๊ทธ์•„์›ƒ ์š”๊ตฌ์‚ฌํ•ญ] + - ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ + - ๋ฉ”์‹œ์ง€: "๋กœ๊ทธ์•„์›ƒ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + - ๋ฒ„ํŠผ: ์ทจ์†Œ/ํ™•์ธ + + [์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ] + - ํ™•์ธ ์„ ํƒ: "์•ˆ์ „ํ•˜๊ฒŒ ๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ โ†’ ๋กœ๊ทธ์ธ ํ™”๋ฉด + - ์ทจ์†Œ ์„ ํƒ: ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‹ซํž˜, ํ˜„์žฌ ํ™”๋ฉด ์œ ์ง€ + +- S/3 + +--- + +2. Event ์„œ๋น„์Šค +1) ์ด๋ฒคํŠธ ๊ด€๋ฆฌ +UFR-EVENT-010: [๋Œ€์‹œ๋ณด๋“œ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ๋‚ด ์ด๋ฒคํŠธ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด | ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ง„ํ–‰์ค‘/์˜ˆ์ •/์ข…๋ฃŒ๋œ ์ด๋ฒคํŠธ๋ฅผ ํ•œ๋ˆˆ์— ํ™•์ธํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ด๋ฒคํŠธ ๋ชฉ๋ก ํ™•์ธ + ๋กœ๊ทธ์ธํ•œ ์ƒํƒœ์—์„œ Bottom Navigation์˜ "ํ™ˆ" ํƒญ์„ ํด๋ฆญํ•˜๊ฑฐ๋‚˜, ์ตœ์ดˆ ๋กœ๊ทธ์ธ ์‹œ ๋Œ€์‹œ๋ณด๋“œ์— ์ ‘๊ทผํ•œ ์ƒํ™ฉ์—์„œ | ์ด๋ฒคํŠธ ๋ชฉ๋ก ์˜์—ญ์„ ํ™•์ธํ•˜๋ฉด | ๋‚ด ์ด๋ฒคํŠธ๋“ค์ด ์ƒํƒœ๋ณ„๋กœ ๊ตฌ๋ถ„๋˜์–ด ํ‘œ์‹œ๋œ๋‹ค. + + [ํ‘œ์‹œ ์š”๊ตฌ์‚ฌํ•ญ] + - ์ด๋ฒคํŠธ ์ƒํƒœ๋ณ„ ์„น์…˜ ๊ตฌ์„ฑ + - "์ง„ํ–‰์ค‘": ํ˜„์žฌ ์ง„ํ–‰ ์ค‘์ธ ์ด๋ฒคํŠธ + - "์˜ˆ์ •": ๋ฐฐํฌ ๋Œ€๊ธฐ ์ค‘์ธ ์ด๋ฒคํŠธ + - "์ข…๋ฃŒ": ์™„๋ฃŒ๋œ ์ด๋ฒคํŠธ + - ์ด๋ฒคํŠธ ์นด๋“œ ์ •๋ณด + - ์ด๋ฒคํŠธ๋ช… + - ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„ + - ์ง„ํ–‰ ์ƒํƒœ ๋ฑƒ์ง€ + - ๊ฐ„๋‹จํ•œ ํ†ต๊ณ„ (์ฐธ์—ฌ์ž ์ˆ˜, ์กฐํšŒ์ˆ˜ ๋“ฑ) + + [์ƒํ˜ธ์ž‘์šฉ ์š”๊ตฌ์‚ฌํ•ญ] + - ์ด๋ฒคํŠธ ์นด๋“œ ํด๋ฆญ: ํ•ด๋‹น ์ด๋ฒคํŠธ ์ƒ์„ธ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + - ์„น์…˜๋‹น ์ตœ๋Œ€ 5๊ฐœ ์นด๋“œ ํ‘œ์‹œ (์ตœ์‹  ์ˆœ) + - "์ „์ฒด๋ณด๊ธฐ" ๋งํฌ: ์ „์ฒด ์ด๋ฒคํŠธ ๋ชฉ๋ก ํ™”๋ฉด์œผ๋กœ ์ด๋™ + - "์ƒˆ ์ด๋ฒคํŠธ ๋งŒ๋“ค๊ธฐ" ๋ฒ„ํŠผ: ์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”„๋กœ์„ธ์Šค ์‹œ์ž‘ + + [๋นˆ ์ƒํƒœ ์ฒ˜๋ฆฌ] + - ์ด๋ฒคํŠธ๊ฐ€ ์—†์„ ๊ฒฝ์šฐ: "์ฒซ ์ด๋ฒคํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”" ์•ˆ๋‚ด ๋ฐ ์ƒ์„ฑ ๋ฒ„ํŠผ + +- S/13 + +--- + +UFR-EVENT-020: [์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ํšจ๊ณผ์ ์ธ ์ด๋ฒคํŠธ๋ฅผ ๊ธฐํšํ•˜๊ธฐ ์œ„ํ•ด | ์ด๋ฒคํŠธ ๋ชฉ์ ์„ ๋จผ์ € ์„ ํƒํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ + ๋Œ€์‹œ๋ณด๋“œ์—์„œ "์ƒˆ ์ด๋ฒคํŠธ ๋งŒ๋“ค๊ธฐ" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•œ ์ƒํ™ฉ์—์„œ | ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ ํ™”๋ฉด์—์„œ ํ•˜๋‚˜์˜ ๋ชฉ์ ์„ ์„ ํƒํ•˜๋ฉด | AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•œ๋‹ค. + + [๋ชฉ์  ์˜ต์…˜] + - ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜: ์ƒˆ๋กœ์šด ๊ณ ๊ฐ ํ™•๋ณด + - ์žฌ๋ฐฉ๋ฌธ ์œ ๋„: ๊ธฐ์กด ๊ณ ๊ฐ ์žฌ๋ฐฉ๋ฌธ ์ด‰์ง„ + - ๋งค์ถœ ์ฆ๋Œ€: ๋‹จ๊ธฐ ๋งค์ถœ ํ–ฅ์ƒ + - ์ธ์ง€๋„ ํ–ฅ์ƒ: ๋ธŒ๋žœ๋“œ/๋งค์žฅ ์ธ์ง€๋„ ์ œ๊ณ  + + [๋ชฉ์ ๋ณ„ ์„ค๋ช… ์ œ๊ณต] + - ๊ฐ ๋ชฉ์ ์— ๋Œ€ํ•œ ๊ฐ„๋‹จํ•œ ์„ค๋ช… + - ์˜ˆ์ƒ ํšจ๊ณผ ์˜ˆ์‹œ + + [๋‹ค์Œ ๋‹จ๊ณ„] + - ๋ชฉ์  ์„ ํƒ ์™„๋ฃŒ ์‹œ ์ž๋™์œผ๋กœ AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + +- M/5 + +--- + +UFR-EVENT-030: [AI์ด๋ฒคํŠธ์ถ”์ฒœ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ์ตœ์‹  ํŠธ๋ Œ๋“œ๋ฅผ ๋ฐ˜์˜ํ•˜๊ณ  ์„ ํƒ์ง€๋ฅผ ๋น„๊ตํ•˜๊ธฐ ์œ„ํ•ด | AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ์™€ ํ•จ๊ป˜ 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ์ถ”์ฒœ์„ ํ™•์ธํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ ํ™•์ธ + ์ด๋ฒคํŠธ ๋ชฉ์ ์„ ์„ ํƒํ•œ ์ƒํ™ฉ์—์„œ | AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ํ™”๋ฉด์— ์ ‘๊ทผํ•˜๋ฉด | ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ์™€ ํ•จ๊ป˜ 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ์ด ํ‘œ์‹œ๋œ๋‹ค. + + [ํ™”๋ฉด ๊ตฌ์„ฑ] + 1. ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ (์ƒ๋‹จ) + - ์—…์ข… ํŠธ๋ Œ๋“œ + - ์ตœ๊ทผ ์„ฑ๊ณตํ•œ ์ด๋ฒคํŠธ ์œ ํ˜• + - ๊ณ ๊ฐ ์„ ํ˜ธ ๊ฒฝํ’ˆ ๋ถ„์„ + - ํšจ๊ณผ์ ์ธ ์ฐธ์—ฌ ๋ฐฉ๋ฒ• + - ์ง€์—ญ ํŠน์„ฑ + - ์ง€์—ญ๋ณ„ ์ด๋ฒคํŠธ ์„ฑ๊ณต๋ฅ  + - ์ง€์—ญ ๊ณ ๊ฐ ํŠน์„ฑ + - ์‹œ์ฆŒ ํŠน์„ฑ + - ๊ณ„์ ˆ/์‹œ๊ธฐ๋ณ„ ์ถ”์ฒœ ์ด๋ฒคํŠธ + - ํŠน๋ณ„ ์ด๋ฒคํŠธ ์‹œ์ฆŒ (๋ช…์ ˆ, ๊ธฐ๋…์ผ ๋“ฑ) + + 2. ์ถ”์ฒœ ์ด๋ฒคํŠธ 3๊ฐ€์ง€ (ํ•˜๋‹จ) + ๊ฐ ์นด๋“œ์— ๋‹ค์Œ ์ •๋ณด ํฌํ•จ: + - ์ด๋ฒคํŠธ ์ œ๋ชฉ (์ˆ˜์ • ๊ฐ€๋Šฅ) + - ์ถ”์ฒœ ๊ฒฝํ’ˆ (์ˆ˜์ • ๊ฐ€๋Šฅ) + - ์ฐธ์—ฌ ๋ฐฉ๋ฒ• + - ์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜ + - ์˜ˆ์ƒ ๋น„์šฉ + - ์˜ˆ์ƒ ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  + + [์ถ”์ฒœ ๊ธฐ์ค€] + - ์ด๋ฒคํŠธ ๋ชฉ์  ๊ธฐ๋ฐ˜ ์ตœ์ ํ™” + - ์—…์ข… ํŠธ๋ Œ๋“œ ๋ฐ˜์˜ + - ์˜ˆ์‚ฐ ๋Œ€๋น„ ํšจ๊ณผ ๊ทน๋Œ€ํ™” + - 3๊ฐ€์ง€ ์˜ต์…˜ ์ฐจ๋ณ„ํ™”: + - ์˜ต์…˜ 1: ์ €๋น„์šฉ, ๋†’์€ ์ฐธ์—ฌ์œจ ์ค‘์‹ฌ + - ์˜ต์…˜ 2: ์ค‘๋น„์šฉ, ๊ท ํ˜•์žกํžŒ ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  + - ์˜ต์…˜ 3: ๊ณ ๋น„์šฉ, ๋†’์€ ๋งค์ถœ ์ฆ๋Œ€ ํšจ๊ณผ + + [๊ฐ„๋‹จํ•œ ์ปค์Šคํ…€ ๊ธฐ๋Šฅ] + - ์ด๋ฒคํŠธ ์ œ๋ชฉ: ๊ฐ ์นด๋“œ์—์„œ ์ง์ ‘ ์ˆ˜์ • ๊ฐ€๋Šฅ (์ตœ๋Œ€ 50์ž) + - ๊ฒฝํ’ˆ๋ช…: ๊ฐ ์นด๋“œ์—์„œ ์ง์ ‘ ์ˆ˜์ • ๊ฐ€๋Šฅ + + [์ด๋ฒคํŠธ ์„ ํƒ] + - 3๊ฐ€์ง€ ์ค‘ 1๊ฐœ ์„ ํƒ (๋ผ๋””์˜ค ๋ฒ„ํŠผ) + - ์„ ํƒํ•œ ์นด๋“œ์˜ ์ปค์Šคํ…€๋œ ์ •๋ณด๊ฐ€ ์ €์žฅ๋จ + - ์„ ํƒ ์™„๋ฃŒ ํ›„ "๋‹ค์Œ" ๋ฒ„ํŠผ ํ™œ์„ฑํ™” + + [ํ”„๋กœ์„ธ์Šค ์‹œ๊ฐ„] + - ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ถ”์ฒœ ์ƒ์„ฑ: 10์ดˆ ์ด๋‚ด + - ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ (๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ) + + [๋‹ค์Œ ๋‹จ๊ณ„] + - ์ด๋ฒคํŠธ ์„ ํƒ ํ›„ ์ฝ˜ํ…์ธ  ์ƒ์„ฑ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + +- M/34 + +- ๊ธฐ์ˆ  ํƒœ์Šคํฌ + - AI ํŠธ๋ Œ๋“œ ๋ถ„์„ API + - ๋น…๋ฐ์ดํ„ฐ ๋ถ„์„: ๊ณผ๊ฑฐ ์„ฑ๊ณต ์ด๋ฒคํŠธ, ์—…์ข…๋ณ„ ์„ฑ๊ณต๋ฅ , ์ง€์—ญ ํŠน์„ฑ, ์‹œ์ฆŒ ํŒจํ„ด + - ํŠธ๋ Œ๋“œ ๋ฆฌํฌํŠธ ์ƒ์„ฑ: JSON ํ˜•ํƒœ ๋ฐ˜ํ™˜ + - ๋ถ„์„ ์™„๋ฃŒ ์‹œ๊ฐ„: 5์ดˆ ์ด๋‚ด + - AI ์ด๋ฒคํŠธ ์ถ”์ฒœ API + - Claude API / GPT-4 API ์—ฐ๋™ + - ๊ธฐํš์•ˆ ์ƒ์„ฑ ๋กœ์ง: + - ๊ฒฝํ’ˆ ์ถ”์ฒœ (์˜ˆ์‚ฐ ๋Œ€๋น„ ๋งค๋ ฅ๋„, ๊ณ ๊ฐ ์„ ํ˜ธ๋„) + - ์ฐธ์—ฌ ๋ฐฉ๋ฒ• ์„ค๊ณ„ (๋‚œ์ด๋„๋ณ„ ์ฐจ๋ณ„ํ™”) + - ํ™๋ณด ๋ฌธ๊ตฌ ์ƒ์„ฑ + - 3๊ฐ€์ง€ ์˜ต์…˜ ์ฐจ๋ณ„ํ™” ์ƒ์„ฑ + - ์˜ˆ์ƒ ์„ฑ๊ณผ ๊ณ„์‚ฐ (์ฐธ์—ฌ์ž ์ˆ˜, ๋น„์šฉ, ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ ) + - ์ƒ์„ฑ ์™„๋ฃŒ ์‹œ๊ฐ„: 5์ดˆ ์ด๋‚ด + - ํ”„๋ก ํŠธ์—”๋“œ ํ†ตํ•ฉ + - ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ ํ‘œ์‹œ + - 3๊ฐ€์ง€ ์ถ”์ฒœ ์นด๋“œ UI + - ์‹ค์‹œ๊ฐ„ ์ œ๋ชฉ/๊ฒฝํ’ˆ ํŽธ์ง‘ ๊ธฐ๋Šฅ + - ์„ ํƒ ์ƒํƒœ ๊ด€๋ฆฌ + - ์„ฑ๋Šฅ ์ตœ์ ํ™” + - ํŠธ๋ Œ๋“œ ๋ถ„์„๊ณผ ์ถ”์ฒœ ์ƒ์„ฑ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ + - ์บ์‹ฑ ์ „๋žต + - ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ + +--- + +UFR-EVENT-040: [๋ฐฐํฌ์ฑ„๋„์„ ํƒ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ํšจ๊ณผ์ ์ธ ์ด๋ฒคํŠธ ํ™๋ณด๋ฅผ ์œ„ํ•ด | ๋‹ค์ค‘ ๋ฐฐํฌ ์ฑ„๋„์„ ์„ ํƒํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ + ์ด๋ฒคํŠธ ์ถ”์ฒœ์„ ์™„๋ฃŒํ•˜๊ณ  ์ฝ˜ํ…์ธ  ์ƒ์„ฑ์ด ์™„๋ฃŒ๋œ ์ƒํ™ฉ์—์„œ | ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ ํ™”๋ฉด์—์„œ ์›ํ•˜๋Š” ์ฑ„๋„์„ ์„ ํƒํ•˜๋ฉด | ๊ฐ ์ฑ„๋„์˜ ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜๊ฐ€ ํ‘œ์‹œ๋œ๋‹ค. + + [๋ฐฐํฌ ์ฑ„๋„ ์˜ต์…˜] + 1. ์šฐ๋ฆฌ๋™๋„คTV + - ๋ฐ˜๊ฒฝ ์„ ํƒ: 500m / 1km + - ์†ก์ถœ ์‹œ๊ฐ„๋Œ€: ํ‰์ผ ์ €๋… / ์ฃผ๋ง ์ ์‹ฌ ๋“ฑ + - ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜ ํ‘œ์‹œ + + 2. ๋ง๊ณ ๋น„์ฆˆ (์—ฐ๊ฒฐ์Œ) + - ๋งค์žฅ ์ „ํ™”๋ฒˆํ˜ธ ์ž๋™ ์—ฐ๋™ + - ์—ฐ๊ฒฐ์Œ ์—…๋ฐ์ดํŠธ ์•ˆ๋‚ด + + 3. ์ง€๋‹ˆTV ๊ด‘๊ณ  + - ํƒ€๊ฒŸ ์ง€์—ญ ์„ ํƒ + - ๋…ธ์ถœ ์‹œ๊ฐ„๋Œ€ ์„ ํƒ + - ์˜ˆ์‚ฐ ์ž…๋ ฅ + - ์˜ˆ์ƒ ๋…ธ์ถœ๋Ÿ‰ ํ‘œ์‹œ + + 4. SNS + - Instagram (์ฒดํฌ๋ฐ•์Šค) + - Naver Blog (์ฒดํฌ๋ฐ•์Šค) + - Kakao Channel (์ฒดํฌ๋ฐ•์Šค) + - ์˜ˆ์•ฝ ์‹œ๊ฐ„ ์„ค์ • + + [์ฑ„๋„๋ณ„ ๋น„์šฉ ์•ˆ๋‚ด] + - ๊ฐ ์ฑ„๋„๋ณ„ ์˜ˆ์ƒ ๋น„์šฉ ํ‘œ์‹œ + - ์ด ์˜ˆ์ƒ ๋น„์šฉ ํ•ฉ๊ณ„ + + [๋‹ค์Œ ๋‹จ๊ณ„] + - ์ตœ์†Œ 1๊ฐœ ์ด์ƒ ์ฑ„๋„ ์„ ํƒ ํ•„์ˆ˜ + - "๋‹ค์Œ" ๋ฒ„ํŠผ: ์ตœ์ข… ์Šน์ธ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + +- M/13 + +--- + +UFR-EVENT-050: [์ตœ์ข…์Šน์ธ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ๋ฐฐํฌํ•˜๊ธฐ ์ „์— | ๋ชจ๋“  ์„ค์ •์„ ์ตœ์ข… ํ™•์ธํ•˜๊ณ  ์Šน์ธํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ์ด๋ฒคํŠธ ์ตœ์ข… ์Šน์ธ + ๋ฐฐํฌ ์ฑ„๋„์„ ์„ ํƒํ•œ ์ƒํ™ฉ์—์„œ | ์ตœ์ข… ์Šน์ธ ํ™”๋ฉด์—์„œ ๋ชจ๋“  ์ •๋ณด๋ฅผ ํ™•์ธํ•˜๊ณ  ์Šน์ธ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด | ์ด๋ฒคํŠธ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ๋ฐฐํฌ๊ฐ€ ์‹œ์ž‘๋œ๋‹ค. + + [์ตœ์ข… ํ™•์ธ ์ •๋ณด] + 1. ์ด๋ฒคํŠธ ์ •๋ณด + - ์ด๋ฒคํŠธ๋ช… + - ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„ + - ์ด๋ฒคํŠธ ๋ชฉ์  + + 2. ๊ฒฝํ’ˆ ์ •๋ณด + - ๊ฒฝํ’ˆ๋ช… + - ๊ฒฝํ’ˆ ์ˆ˜๋Ÿ‰ + + 3. ์ฐธ์—ฌ ๋ฐฉ๋ฒ• + - ์ฐธ์—ฌ ์กฐ๊ฑด ์š”์•ฝ + + 4. ๋ฐฐํฌ ์ฑ„๋„ + - ์„ ํƒํ•œ ์ฑ„๋„ ๋ชฉ๋ก + - ์ฑ„๋„๋ณ„ ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜ + + 5. ์˜ˆ์ƒ ๋น„์šฉ ๋ฐ ํšจ๊ณผ + - ์ด ์˜ˆ์ƒ ๋น„์šฉ + - ์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜ + - ์˜ˆ์ƒ ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  + + [์Šน์ธ ํ”„๋กœ์„ธ์Šค] + - "์Šน์ธ ๋ฐ ๋ฐฐํฌ" ๋ฒ„ํŠผ ํด๋ฆญ + - ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ: "์ด๋ฒคํŠธ๋ฅผ ์Šน์ธํ•˜๊ณ  ๋ฐฐํฌํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + - ์Šน์ธ ์‹œ ์ด๋ฒคํŠธ ์ƒ์„ฑ ๋ฐ ์ฝ˜ํ…์ธ  ์ƒ์„ฑ ์‹œ์ž‘ + + [์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ] + - ์„ฑ๊ณต: "์ด๋ฒคํŠธ๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐฐํฌ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค." ๋ฉ”์‹œ์ง€ + - ์ž๋™์œผ๋กœ ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ + - ๋ฐฐํฌ ์ง„ํ–‰ ์ƒํ™ฉ ์•Œ๋ฆผ + +- M/13 + +--- + +UFR-EVENT-060: [์ด๋ฒคํŠธ์ƒ์„ธ์กฐํšŒ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ์ง„ํ–‰ ์ค‘์ธ ์ด๋ฒคํŠธ์˜ ํ˜„ํ™ฉ์„ ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด | ์ด๋ฒคํŠธ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ์ด๋ฒคํŠธ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ + ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ด๋ฒคํŠธ ์นด๋“œ๋ฅผ ํด๋ฆญํ•œ ์ƒํ™ฉ์—์„œ | ์ด๋ฒคํŠธ ์ƒ์„ธ ํ™”๋ฉด์— ์ ‘๊ทผํ•˜๋ฉด | ์ด๋ฒคํŠธ์˜ ๋ชจ๋“  ์ •๋ณด์™€ ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„๊ฐ€ ํ‘œ์‹œ๋œ๋‹ค. + + [์ƒ์„ธ ์ •๋ณด ๊ตฌ์„ฑ] + 1. ๊ธฐ๋ณธ ์ •๋ณด + - ์ด๋ฒคํŠธ๋ช… + - ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„ + - ์ง„ํ–‰ ์ƒํƒœ + - ๊ฒฝํ’ˆ ์ •๋ณด + - ์ฐธ์—ฌ ๋ฐฉ๋ฒ• + + 2. ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ (Analytics ์„œ๋น„์Šค ์—ฐ๋™) + - ์ฐธ์—ฌ์ž ์ˆ˜ + - ๋…ธ์ถœ ์ˆ˜ + - ์กฐํšŒ ์ˆ˜ + - ๊ณต์œ  ์ˆ˜ + + 3. ๋ฐฐํฌ ์ฑ„๋„ ํ˜„ํ™ฉ + - ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ์ƒํƒœ + - ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ์š”์•ฝ + + 4. ์ฐธ์—ฌ์ž ๋ชฉ๋ก + - ์ตœ๊ทผ ์ฐธ์—ฌ์ž 10๋ช… + - "์ „์ฒด ์ฐธ์—ฌ์ž ๋ณด๊ธฐ" ๋งํฌ + + [์•ก์…˜ ๋ฒ„ํŠผ] + - "ํšจ๊ณผ ์ธก์ • ๋Œ€์‹œ๋ณด๋“œ": Analytics ๋Œ€์‹œ๋ณด๋“œ๋กœ ์ด๋™ + - "์ด๋ฒคํŠธ ์ˆ˜์ •": ์ง„ํ–‰ ์ค‘์ธ ์ด๋ฒคํŠธ ์ˆ˜์ • (์ œํ•œ์ ) + - "์ด๋ฒคํŠธ ์ข…๋ฃŒ": ์˜ˆ์ •๋ณด๋‹ค ๋นจ๋ฆฌ ์ข…๋ฃŒ + +- S/13 + +--- + +UFR-EVENT-070: [์ด๋ฒคํŠธ๋ชฉ๋ก๊ด€๋ฆฌ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ๋ชจ๋“  ์ด๋ฒคํŠธ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด | ์ „์ฒด ์ด๋ฒคํŠธ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๊ณ  ํ•„ํ„ฐ๋ง/๊ฒ€์ƒ‰ํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ์ „์ฒด ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ + Bottom Navigation์˜ "์ด๋ฒคํŠธ" ํƒญ์„ ํด๋ฆญํ•˜๊ฑฐ๋‚˜, ๋Œ€์‹œ๋ณด๋“œ์—์„œ "์ „์ฒด๋ณด๊ธฐ" ๋งํฌ๋ฅผ ํด๋ฆญํ•œ ์ƒํ™ฉ์—์„œ | ์ด๋ฒคํŠธ ๋ชฉ๋ก ํ™”๋ฉด์— ์ ‘๊ทผํ•˜๋ฉด | ๋ชจ๋“  ์ด๋ฒคํŠธ๊ฐ€ ํ…Œ์ด๋ธ” ํ˜•ํƒœ๋กœ ํ‘œ์‹œ๋œ๋‹ค. + + [๋ชฉ๋ก ํ‘œ์‹œ ์š”๊ตฌ์‚ฌํ•ญ] + - ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ + - ์ด๋ฒคํŠธ๋ช… + - ์ด๋ฒคํŠธ ๊ธฐ๊ฐ„ + - ์ƒํƒœ (์ง„ํ–‰์ค‘/์˜ˆ์ •/์ข…๋ฃŒ) + - ์ฐธ์—ฌ์ž ์ˆ˜ + - ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  + - ์ƒ์„ฑ์ผ + - ํŽ˜์ด์ง€๋„ค์ด์…˜ (ํŽ˜์ด์ง€๋‹น 20๊ฐœ) + + [ํ•„ํ„ฐ ์˜ต์…˜] + - ์ƒํƒœ๋ณ„ ํ•„ํ„ฐ: ์ „์ฒด/์ง„ํ–‰์ค‘/์˜ˆ์ •/์ข…๋ฃŒ + - ๊ธฐ๊ฐ„๋ณ„ ํ•„ํ„ฐ: ์ตœ๊ทผ 1๊ฐœ์›”/3๊ฐœ์›”/6๊ฐœ์›”/1๋…„/์ „์ฒด + + [๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ] + - ๊ฒ€์ƒ‰์ฐฝ: ์ด๋ฒคํŠธ๋ช… ๊ฒ€์ƒ‰ + - ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ + + [์ •๋ ฌ ์˜ต์…˜] + - ์ตœ์‹ ์ˆœ (๊ธฐ๋ณธ๊ฐ’) + - ์ฐธ์—ฌ์ž ๋งŽ์€ ์ˆœ + - ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  ๋†’์€ ์ˆœ + + [์ด๋ฒคํŠธ ๊ด€๋ฆฌ] + - ํ–‰ ํด๋ฆญ: ์ด๋ฒคํŠธ ์ƒ์„ธ ํ™”๋ฉด + - ๋‹ค์ค‘ ์„ ํƒ: ์ผ๊ด„ ์‚ญ์ œ ๊ธฐ๋Šฅ + +- S/13 + +--- + +3. AI ์„œ๋น„์Šค +1) AI ๋ถ„์„ ๋ฐ ์ถ”์ฒœ +UFR-AI-010: [AIํŠธ๋ Œ๋“œ๋ถ„์„๋ฐ์ด๋ฒคํŠธ์ถ”์ฒœ] AI ์‹œ์Šคํ…œ์œผ๋กœ์„œ | ๋‚˜๋Š” ์†Œ์ƒ๊ณต์ธ์—๊ฒŒ ์ตœ์ ์˜ ์ด๋ฒคํŠธ๋ฅผ ์ œ์•ˆํ•˜๊ธฐ ์œ„ํ•ด | ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ๋ฅผ ๋ถ„์„ํ•˜๊ณ  3๊ฐ€์ง€ ์ตœ์ ํ™”๋œ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ์„ ์ƒ์„ฑํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ ์ˆ˜ํ–‰ + ์†Œ์ƒ๊ณต์ธ์ด ์ด๋ฒคํŠธ ๋ชฉ์ ์„ ์„ ํƒํ•œ ์ƒํ™ฉ์—์„œ | AI ์‹œ์Šคํ…œ์ด ์š”์ฒญ์„ ๋ฐ›์œผ๋ฉด | ํŠธ๋ Œ๋“œ ๋ถ„์„๊ณผ ์ด๋ฒคํŠธ ์ถ”์ฒœ์„ ๋ณ‘๋ ฌ๋กœ ์ˆ˜ํ–‰ํ•˜์—ฌ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. + + [๋ถ„์„ ๋ฐ์ดํ„ฐ ์†Œ์Šค] + - ๊ณผ๊ฑฐ ์„ฑ๊ณต ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ + - ์—…์ข…๋ณ„ ์ด๋ฒคํŠธ ์„ฑ๊ณต๋ฅ  + - ์ง€์—ญ๋ณ„ ๊ณ ๊ฐ ํŠน์„ฑ + - ์‹œ์ฆŒ๋ณ„ ํŠธ๋ Œ๋“œ ํŒจํ„ด + + [ํŠธ๋ Œ๋“œ ๋ถ„์„ ํ•ญ๋ชฉ] + 1. ์—…์ข… ํŠธ๋ Œ๋“œ + - ์ตœ๊ทผ 3๊ฐœ์›” ์„ฑ๊ณต ์ด๋ฒคํŠธ ์œ ํ˜• + - ๊ณ ๊ฐ ์„ ํ˜ธ ๊ฒฝํ’ˆ Top 5 + - ํšจ๊ณผ์ ์ธ ์ฐธ์—ฌ ๋ฐฉ๋ฒ• ๋ถ„์„ + + 2. ์ง€์—ญ ํŠน์„ฑ + - ํ•ด๋‹น ์ง€์—ญ ์ด๋ฒคํŠธ ์„ฑ๊ณต๋ฅ  + - ์ง€์—ญ ๊ณ ๊ฐ ์—ฐ๋ น๋Œ€/์„ฑ๋ณ„ ๋ถ„ํฌ + + 3. ์‹œ์ฆŒ ํŠน์„ฑ + - ๊ณ„์ ˆ๋ณ„ ์ถ”์ฒœ ์ด๋ฒคํŠธ + - ํŠน๋ณ„ ์‹œ์ฆŒ (๋ช…์ ˆ, ๊ธฐ๋…์ผ) ๋ฐ˜์˜ + + [์ด๋ฒคํŠธ ์ถ”์ฒœ ๋กœ์ง] + 1. ๊ฒฝํ’ˆ ์ถ”์ฒœ + - ์˜ˆ์‚ฐ ๋Œ€๋น„ ๋งค๋ ฅ๋„ ์ตœ๋Œ€ํ™” + - ํƒ€๊ฒŸ ๊ณ ๊ฐ ์„ ํ˜ธ๋„ ๋ฐ˜์˜ + - KT ๋ฉค๋ฒ„์‹ญ ํฌ์ธํŠธ ํ™œ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ + + 2. ์ฐธ์—ฌ ๋ฐฉ๋ฒ• ์„ค๊ณ„ + - ๊ฐ„๋‹จํ•œ ์ฐธ์—ฌ ๋ฐฉ๋ฒ• (์ € ๋‚œ์ด๋„) + - ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ ์š”์†Œ (์ค‘ ๋‚œ์ด๋„) + - ๋ฐ”์ด๋Ÿด ํ™•์‚ฐ ์žฅ์น˜ (๊ณ  ๋‚œ์ด๋„) + + 3. ํ™๋ณด ๋ฌธ๊ตฌ ์ƒ์„ฑ + - ์ด๋ฒคํŠธ ๊ฐœ์š” ๊ธฐ๋ฐ˜ ๋ฌธ๊ตฌ 5๊ฐœ ์ƒ์„ฑ + - SNS ํ•ด์‹œํƒœ๊ทธ ์ž๋™ ์ƒ์„ฑ + + [3๊ฐ€์ง€ ์˜ต์…˜ ์ฐจ๋ณ„ํ™”] + - ์˜ต์…˜ 1: ์ €๋น„์šฉ, ๋†’์€ ์ฐธ์—ฌ์œจ ์ค‘์‹ฌ + - ์˜ต์…˜ 2: ์ค‘๋น„์šฉ, ๊ท ํ˜•์žกํžŒ ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  + - ์˜ต์…˜ 3: ๊ณ ๋น„์šฉ, ๋†’์€ ๋งค์ถœ ์ฆ๋Œ€ ํšจ๊ณผ + + [์˜ˆ์ƒ ์„ฑ๊ณผ ๊ณ„์‚ฐ] + - ์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜ + - ์˜ˆ์ƒ ๋น„์šฉ + - ์˜ˆ์ƒ ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  + + [๊ฒฐ๊ณผ ์ถœ๋ ฅ] + - ํŠธ๋ Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ JSON ํ˜•ํƒœ ๋ฐ˜ํ™˜ + - 3๊ฐ€์ง€ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ JSON ๋ฐฐ์—ด ๋ฐ˜ํ™˜ + - ์ „์ฒด ์™„๋ฃŒ ์‹œ๊ฐ„: 10์ดˆ ์ด๋‚ด (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) + +- M/34 + +- ๊ธฐ์ˆ  ํƒœ์Šคํฌ + - AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ์—”์ง„ + - ๋น…๋ฐ์ดํ„ฐ ๋ถ„์„ ์‹œ์Šคํ…œ ์—ฐ๋™ + - ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ๋ณ„ ๋ฐ์ดํ„ฐ ์ง‘๊ณ„ + - ํŠธ๋ Œ๋“œ ํŒจํ„ด ์ธ์‹ ์•Œ๊ณ ๋ฆฌ์ฆ˜ + - AI ์ด๋ฒคํŠธ ์ถ”์ฒœ ์—”์ง„ + - Claude API / GPT-4 API ์—ฐ๋™ + - ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง ์ตœ์ ํ™” + - ์‘๋‹ต ํŒŒ์‹ฑ ๋ฐ ๊ตฌ์กฐํ™” + - ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ์•„ํ‚คํ…์ฒ˜ + - ํŠธ๋ Œ๋“œ ๋ถ„์„๊ณผ ์ด๋ฒคํŠธ ์ถ”์ฒœ ๋™์‹œ ์‹คํ–‰ + - ๊ฒฐ๊ณผ ํ†ตํ•ฉ ๋กœ์ง + - ์„ฑ๋Šฅ ์ตœ์ ํ™” + - ์บ์‹ฑ ์ „๋žต (Redis) + - ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ + - ์‘๋‹ต ์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง + +--- + +4. Content ์„œ๋น„์Šค +1) ์ฝ˜ํ…์ธ  ์ƒ์„ฑ +UFR-CONT-010: [SNS์ด๋ฏธ์ง€์ƒ์„ฑ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” SNS์— ๊ฒŒ์‹œํ•  ์ด๋ฒคํŠธ ์ด๋ฏธ์ง€๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด | AI๊ฐ€ ์ž๋™์œผ๋กœ 3๊ฐ€์ง€ ์Šคํƒ€์ผ์˜ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ๋ฅผ ์›ํ•œ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: SNS ์ด๋ฏธ์ง€ ์ž๋™ ์ƒ์„ฑ + ์ด๋ฒคํŠธ ์ถ”์ฒœ์„ ์™„๋ฃŒํ•œ ์ƒํ™ฉ์—์„œ | SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ™”๋ฉด์— ์ ‘๊ทผํ•˜๋ฉด | AI๊ฐ€ 3๊ฐ€์ง€ ์Šคํƒ€์ผ์˜ SNS ์ด๋ฏธ์ง€๋ฅผ ์ž๋™ ์ƒ์„ฑํ•œ๋‹ค. + + [์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ž…๋ ฅ] + - ์ด๋ฒคํŠธ ์ œ๋ชฉ + - ๊ฒฝํ’ˆ ์ •๋ณด + - ๋ธŒ๋žœ๋“œ ์ปฌ๋Ÿฌ (ํ”„๋กœํ•„์—์„œ ๊ฐ€์ ธ์˜ด) + - ๋กœ๊ณ  ์ด๋ฏธ์ง€ (์—…๋กœ๋“œ๋œ ๊ฒฝ์šฐ) + + [3๊ฐ€์ง€ ์Šคํƒ€์ผ ์นด๋“œ ์„ ํƒ] + 1. ์‹ฌํ”Œ ์Šคํƒ€์ผ + - ๊น”๋”ํ•œ ๋””์ž์ธ + - ํ…์ŠคํŠธ ์ค‘์‹ฌ + - ์ฝ๊ธฐ ์‰ฌ์šด ๊ตฌ์„ฑ + - ์นด๋“œ ํ˜•ํƒœ๋กœ ์ œ๊ณต (์„ ํƒ ๊ฐ€๋Šฅ) + + 2. ํ™”๋ คํ•œ ์Šคํƒ€์ผ + - ๋ˆˆ์— ๋„๋Š” ๋””์ž์ธ + - ์ด๋ฏธ์ง€ ๊ฐ•์กฐ + - ํ’๋ถ€ํ•œ ์ƒ‰์ƒ + - ์นด๋“œ ํ˜•ํƒœ๋กœ ์ œ๊ณต (์„ ํƒ ๊ฐ€๋Šฅ) + + 3. ํŠธ๋ Œ๋”” ์Šคํƒ€์ผ + - ์ตœ์‹  ํŠธ๋ Œ๋“œ ๋ฐ˜์˜ + - SNS ์ตœ์ ํ™” + - MZ์„ธ๋Œ€ ํƒ€๊ฒŸ + - ์นด๋“œ ํ˜•ํƒœ๋กœ ์ œ๊ณต (์„ ํƒ ๊ฐ€๋Šฅ) + + [ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™”] + - Instagram: 1080x1080 + - Naver Blog: 800x600 + - Kakao Channel: 800x800 + + [์ƒ์„ฑ ํ”„๋กœ์„ธ์Šค] + - ์ƒ์„ฑ ์ง„ํ–‰ ํ‘œ์‹œ (์Šคํ”ผ๋„ˆ ์• ๋‹ˆ๋ฉ”์ด์…˜) + - "๋”ฅ๋Ÿฌ๋‹ ๋ชจ๋ธ์ด ์ด๋ฒคํŠธ์— ์–ด์šธ๋ฆฌ๋Š” ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์žˆ์–ด์š”..." ์•ˆ๋‚ด + - ์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„: 5์ดˆ ์ด๋‚ด + - ์ƒ์„ฑ ์™„๋ฃŒ ์‹œ 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์นด๋“œ ํ‘œ์‹œ + + [์ด๋ฏธ์ง€ ์„ ํƒ ๋ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ] + - 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์ค‘ 1๊ฐœ ์„ ํƒ ๊ฐ€๋Šฅ (์นด๋“œ ์„ ํƒ ๋ฐฉ์‹) + - ๊ฐ ์นด๋“œ ํด๋ฆญ ์‹œ ํ’€์Šคํฌ๋ฆฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ œ๊ณต + - "๋‹ค์‹œ ์ƒ์„ฑ" ์˜ต์…˜ (์ตœ๋Œ€ 3ํšŒ) + - ์„ ํƒ ์™„๋ฃŒ ์‹œ "๋‹ค์Œ" ๋ฒ„ํŠผ ํ™œ์„ฑํ™” + +- M/21 + +--- + +UFR-CONT-020: [์ฝ˜ํ…์ธ ํŽธ์ง‘] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€๋ฅผ ๋‚ด ์Šคํƒ€์ผ์— ๋งž๊ฒŒ ์กฐ์ •ํ•˜๊ธฐ ์œ„ํ•ด | ๊ฐ„๋‹จํ•œ ํŽธ์ง‘ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ์ƒ์„ฑ๋œ ์ฝ˜ํ…์ธ  ํŽธ์ง‘ + SNS ์ด๋ฏธ์ง€ ์Šคํƒ€์ผ์„ ์„ ํƒํ•œ ์ƒํ™ฉ์—์„œ | ์ฝ˜ํ…์ธ  ํŽธ์ง‘ ํ™”๋ฉด์—์„œ ํ…์ŠคํŠธ๋‚˜ ์ƒ‰์ƒ์„ ์ˆ˜์ •ํ•˜๋ฉด | ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ์—…๋ฐ์ดํŠธ๋œ๋‹ค. + + [ํŽธ์ง‘ ๊ฐ€๋Šฅ ํ•ญ๋ชฉ] + - ํ…์ŠคํŠธ ์ˆ˜์ • + - ์ œ๋ชฉ ํ…์ŠคํŠธ (์ธ๋ผ์ธ ํŽธ์ง‘) + - ๊ฒฝํ’ˆ ์ •๋ณด ํ…์ŠคํŠธ (์ธ๋ผ์ธ ํŽธ์ง‘) + - ์ฐธ์—ฌ ์•ˆ๋‚ด ํ…์ŠคํŠธ (์ธ๋ผ์ธ ํŽธ์ง‘) + - ์ƒ‰์ƒ ์กฐ์ • + - ๋ฐฐ๊ฒฝ์ƒ‰ (์ปฌ๋Ÿฌ ํ”ผ์ปค) + - ํ…์ŠคํŠธ ์ƒ‰์ƒ (์ปฌ๋Ÿฌ ํ”ผ์ปค) + - ๊ฐ•์กฐ ์ƒ‰์ƒ (์ปฌ๋Ÿฌ ํ”ผ์ปค) + + [ํŽธ์ง‘ ์ œ์•ฝ์‚ฌํ•ญ] + - ๋ ˆ์ด์•„์›ƒ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€ + - ๋กœ๊ณ  ์œ„์น˜/ํฌ๊ธฐ ์กฐ์ ˆ ๋ถˆ๊ฐ€ + - ์ด๋ฏธ์ง€ ๊ต์ฒด ๋ถˆ๊ฐ€ (์ด์ „ ํ™”๋ฉด์˜ "๋‹ค์‹œ ์ƒ์„ฑ"์œผ๋กœ๋งŒ ๊ฐ€๋Šฅ) + + [์‹ค์‹œ๊ฐ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ] + - ์ˆ˜์ •์‚ฌํ•ญ ์ฆ‰์‹œ ๋ฐ˜์˜ + - ํ”Œ๋žซํผ๋ณ„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ „ํ™˜ ๊ฐ€๋Šฅ (Instagram/Naver/Kakao) + - ๋ชจ๋ฐ”์ผ ์ƒ๋‹จ์— ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์˜์—ญ ํ‘œ์‹œ + + [์ €์žฅ ๋ฐ ์™„๋ฃŒ] + - "์ €์žฅ" ๋ฒ„ํŠผ: ํŽธ์ง‘ ๋‚ด์šฉ ์ž„์‹œ ์ €์žฅ + - "๋‹ค์Œ" ๋ฒ„ํŠผ: ๋ฐฐํฌ ์ฑ„๋„ ์„ ํƒ ํ™”๋ฉด์œผ๋กœ ์ด๋™ + +- S/13 + +--- + +5. Distribution ์„œ๋น„์Šค +1) ๋ฐฐํฌ ๊ด€๋ฆฌ +UFR-DIST-010: [๋‹ค์ค‘์ฑ„๋„๋ฐฐํฌ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ์—ฌ๋Ÿฌ ์ฑ„๋„์— ๋™์‹œ์— ์ด๋ฒคํŠธ๋ฅผ ํ™๋ณดํ•˜๊ธฐ ์œ„ํ•ด | ์›ํด๋ฆญ์œผ๋กœ ๋‹ค์ค‘ ์ฑ„๋„ ๋ฐฐํฌ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ๋‹ค์ค‘ ์ฑ„๋„ ๋™์‹œ ๋ฐฐํฌ + ์ตœ์ข… ์Šน์ธ์ด ์™„๋ฃŒ๋œ ์ƒํ™ฉ์—์„œ | Distribution ์„œ๋น„์Šค๊ฐ€ ๋ฐฐํฌ ์š”์ฒญ์„ ๋ฐ›์œผ๋ฉด | ์„ ํƒ๋œ ๋ชจ๋“  ์ฑ„๋„์— ๋™์‹œ ๋ฐฐํฌ๋ฅผ ์‹œ์ž‘ํ•œ๋‹ค. + + [๋ฐฐํฌ ์ฑ„๋„ ์ฒ˜๋ฆฌ] + 1. ์šฐ๋ฆฌ๋™๋„คTV + - ์šฐ๋ฆฌ๋™๋„คTV API ์—ฐ๋™ + - 15์ดˆ ์˜์ƒ ์—…๋กœ๋“œ (Content ์„œ๋น„์Šค์—์„œ ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ํ™œ์šฉ) + - ์†ก์ถœ ์‹œ๊ฐ„๋Œ€ ๋ฐ ๋ฐ˜๊ฒฝ ์„ค์ • ์ „๋‹ฌ + - ๋ฐฐํฌ ์™„๋ฃŒ ์‹œ ๋ฐฐํฌ ID ์ˆ˜์‹  + + 2. ๋ง๊ณ ๋น„์ฆˆ (์—ฐ๊ฒฐ์Œ) + - ๋ง๊ณ ๋น„์ฆˆ API ์—ฐ๋™ + - ๋งค์žฅ ์ „ํ™”๋ฒˆํ˜ธ๋กœ ์—ฐ๊ฒฐ์Œ ์—…๋ฐ์ดํŠธ + - ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ ์‹œ๊ฐ ์ˆ˜์‹  + + 3. ์ง€๋‹ˆTV ๊ด‘๊ณ  + - ์ง€๋‹ˆTV ๊ด‘๊ณ  API ์—ฐ๋™ + - ํƒ€๊ฒŸ ์ง€์—ญ, ๋…ธ์ถœ ์‹œ๊ฐ„๋Œ€, ์˜ˆ์‚ฐ ์ •๋ณด ์ „๋‹ฌ + - ๊ด‘๊ณ  ID ๋ฐ ๋…ธ์ถœ ์Šค์ผ€์ค„ ์ˆ˜์‹  + + 4. SNS ์ž๋™ ํฌ์ŠคํŒ… + - Instagram, Naver Blog, Kakao Channel ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ + - ํ”Œ๋žซํผ๋ณ„ ์ตœ์ ํ™”๋œ ์ด๋ฏธ์ง€ ์‚ฌ์šฉ + - ์˜ˆ์•ฝ ์‹œ๊ฐ„ ์„ค์ • ๋ฐ˜์˜ + - ๊ฐ ํ”Œ๋žซํผ ํฌ์ŠคํŒ… ์™„๋ฃŒ ํ™•์ธ + + [๋ฐฐํฌ ์ง„ํ–‰ ์ƒํ™ฉ] + - ์‹ค์‹œ๊ฐ„ ์ง„ํ–‰ ์ƒํ™ฉ ์•Œ๋ฆผ + - ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ์ƒํƒœ ํ‘œ์‹œ + - ์˜ˆ์ƒ ์™„๋ฃŒ ์‹œ๊ฐ„: 1๋ถ„ ์ด๋‚ด + + [๋ฐฐํฌ ์‹คํŒจ ์ฒ˜๋ฆฌ] + - ์ฑ„๋„๋ณ„ ๋…๋ฆฝ์  ์ฒ˜๋ฆฌ (ํ•˜๋‚˜ ์‹คํŒจํ•ด๋„ ๋‹ค๋ฅธ ์ฑ„๋„ ๊ณ„์† ์ง„ํ–‰) + - ์‹คํŒจ ์‹œ ์ž๋™ ์žฌ์‹œ๋„ (์ตœ๋Œ€ 3ํšŒ) + - ์ตœ์ข… ์‹คํŒจ ์‹œ ์†Œ์ƒ๊ณต์ธ์—๊ฒŒ ์•Œ๋ฆผ + + [๋ฐฐํฌ ์™„๋ฃŒ ์ฒ˜๋ฆฌ] + - ๋ฐฐํฌ ์ด๋ ฅ DB ์ €์žฅ + - ๋ฐฐํฌ ์ฑ„๋„ ๋ชฉ๋ก ๋ฐ ์˜ˆ์ƒ ๋„๋‹ฌ ์ˆ˜ ๋ฐ˜ํ™˜ + - ์†Œ์ƒ๊ณต์ธ์—๊ฒŒ ๋ฐฐํฌ ์™„๋ฃŒ ์•Œ๋ฆผ (์•ฑ ํ‘ธ์‹œ/์ด๋ฉ”์ผ) + +- M/21 + +--- + +UFR-DIST-020: [๋ฐฐํฌ์ƒํƒœ๋ชจ๋‹ˆํ„ฐ๋ง] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ๋ฐฐํฌ ์ง„ํ–‰ ์ƒํ™ฉ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด | ๋ฐฐํฌ ์ƒํƒœ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ๋ฐฐํฌ ์ƒํƒœ ์‹ค์‹œ๊ฐ„ ํ™•์ธ + ๋ฐฐํฌ๊ฐ€ ์‹œ์ž‘๋œ ์ƒํ™ฉ์—์„œ | ์ด๋ฒคํŠธ ์ƒ์„ธ ํ™”๋ฉด์˜ ๋ฐฐํฌ ์ƒํƒœ ์„น์…˜์„ ํ™•์ธํ•˜๋ฉด | ์ฑ„๋„๋ณ„ ๋ฐฐํฌ ์ง„ํ–‰ ์ƒํ™ฉ์ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ‘œ์‹œ๋œ๋‹ค. + + [๋ฐฐํฌ ์ƒํƒœ ํ‘œ์‹œ] + - ์ฑ„๋„๋ณ„ ์ƒํƒœ + - ๋Œ€๊ธฐ์ค‘: ํšŒ์ƒ‰ + - ์ง„ํ–‰์ค‘: ํŒŒ๋ž€์ƒ‰ (์ง„ํ–‰๋ฅ  ํ‘œ์‹œ) + - ์™„๋ฃŒ: ์ดˆ๋ก์ƒ‰ (์ฒดํฌ ํ‘œ์‹œ) + - ์‹คํŒจ: ๋นจ๊ฐ„์ƒ‰ (์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€) + + [๋ฐฐํฌ ์ •๋ณด] + - ์šฐ๋ฆฌ๋™๋„คTV: ๋ฐฐํฌ ID, ์˜ˆ์ƒ ๋…ธ์ถœ ์ˆ˜ + - ๋ง๊ณ ๋น„์ฆˆ: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ ์‹œ๊ฐ + - ์ง€๋‹ˆTV: ๊ด‘๊ณ  ID, ๋…ธ์ถœ ์Šค์ผ€์ค„ + - SNS: ํฌ์ŠคํŒ… URL ๋งํฌ + + [์žฌ์‹œ๋„/์ทจ์†Œ] + - ์‹คํŒจํ•œ ์ฑ„๋„: "์žฌ์‹œ๋„" ๋ฒ„ํŠผ + - ์ง„ํ–‰์ค‘์ธ ๋ฐฐํฌ: "์ทจ์†Œ" ๋ฒ„ํŠผ (์ผ๋ถ€ ์ฑ„๋„๋งŒ ๊ฐ€๋Šฅ) + +- S/13 + +--- + +6. Participation ์„œ๋น„์Šค +1) ์ฐธ์—ฌ์ž ๊ด€๋ฆฌ +UFR-PART-010: [์ด๋ฒคํŠธ์ฐธ์—ฌ] ๊ณ ๊ฐ์œผ๋กœ์„œ | ๋‚˜๋Š” ๊ฐ„ํŽธํ•˜๊ฒŒ ์ด๋ฒคํŠธ์— ์ฐธ์—ฌํ•˜๊ธฐ ์œ„ํ•ด | ์ตœ์†Œํ•œ์˜ ์ •๋ณด๋งŒ ์ž…๋ ฅํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ๊ณ ๊ฐ ์ด๋ฒคํŠธ ์ฐธ์—ฌ + ๊ณ ๊ฐ์ด ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ๊ฒฌํ•œ ์ƒํ™ฉ์—์„œ | ์ด๋ฒคํŠธ ์ฐธ์—ฌ ํ™”๋ฉด์—์„œ ํ•„์ˆ˜ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์ฐธ์—ฌ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด | ์ฐธ์—ฌ ์ ‘์ˆ˜๊ฐ€ ์™„๋ฃŒ๋œ๋‹ค. + + [์ด๋ฒคํŠธ ๋ฐœ๊ฒฌ ๊ฒฝ๋กœ] + - ์šฐ๋ฆฌ๋™๋„คTV + - SNS (Instagram, Blog, Kakao) + - ๋งค์žฅ ๋ฐฉ๋ฌธ (๋ง๊ณ ๋น„์ฆˆ ์—ฐ๊ฒฐ์Œ ์•ˆ๋‚ด) + + [์ฐธ์—ฌ ์ •๋ณด ์ž…๋ ฅ] + - ์ด๋ฆ„: ํ•„์ˆ˜ (2์ž ์ด์ƒ) + - ์ „ํ™”๋ฒˆํ˜ธ: ํ•„์ˆ˜ (ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ) + - ์ฐธ์—ฌ ๊ฒฝ๋กœ: ์ž๋™ ๊ฐ์ง€ ๋˜๋Š” ์„ ํƒ + + [์ค‘๋ณต ์ฐธ์—ฌ ์ฒดํฌ] + - ์ „ํ™”๋ฒˆํ˜ธ ๊ธฐ๋ฐ˜ ์ค‘๋ณต ํ™•์ธ + - 1์ธ 1ํšŒ ์ฐธ์—ฌ ์ œํ•œ + - ์ค‘๋ณต ์‹œ: "์ด๋ฏธ ์ฐธ์—ฌํ•˜์‹  ์ด๋ฒคํŠธ์ž…๋‹ˆ๋‹ค" ์•ˆ๋‚ด + + [์ฐธ์—ฌ ์ ‘์ˆ˜ ์™„๋ฃŒ] + - ์‘๋ชจ ๋ฒˆํ˜ธ ๋ฐœ๊ธ‰ + - ๋‹น์ฒจ ๋ฐœํ‘œ์ผ ์•ˆ๋‚ด + - ์ฐธ์—ฌ ์™„๋ฃŒ ํ™”๋ฉด ํ‘œ์‹œ + + [๊ฐœ์ธ์ •๋ณด ์ฒ˜๋ฆฌ] + - ๊ฐœ์ธ์ •๋ณด ์ˆ˜์ง‘/์ด์šฉ ๋™์˜ ํ•„์ˆ˜ + - ๋งˆ์ผ€ํŒ… ํ™œ์šฉ ๋™์˜ ์„ ํƒ + +- M/13 + +--- + +UFR-PART-020: [์ฐธ์—ฌ์ž๋ชฉ๋ก์กฐํšŒ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ๋ˆ„๊ฐ€ ์ฐธ์—ฌํ–ˆ๋Š”์ง€ ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด | ์ฐธ์—ฌ์ž ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ์ฐธ์—ฌ์ž ๋ชฉ๋ก ํ™•์ธ + ์ด๋ฒคํŠธ ์ƒ์„ธ ํ™”๋ฉด์—์„œ "์ฐธ์—ฌ์ž ๋ชฉ๋ก" ํƒญ์„ ํด๋ฆญํ•œ ์ƒํ™ฉ์—์„œ | ์ฐธ์—ฌ์ž ๋ชฉ๋ก ํ™”๋ฉด์— ์ ‘๊ทผํ•˜๋ฉด | ๋ชจ๋“  ์ฐธ์—ฌ์ž ์ •๋ณด๊ฐ€ ํ…Œ์ด๋ธ”๋กœ ํ‘œ์‹œ๋œ๋‹ค. + + [๋ชฉ๋ก ํ‘œ์‹œ] + - ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ + - ๋ฒˆํ˜ธ (์‘๋ชจ ๋ฒˆํ˜ธ) + - ์ด๋ฆ„ + - ์ „ํ™”๋ฒˆํ˜ธ (์ผ๋ถ€ ๋งˆ์Šคํ‚น) + - ์ฐธ์—ฌ ๊ฒฝ๋กœ + - ์ฐธ์—ฌ ์ผ์‹œ + - ๋‹น์ฒจ ์—ฌ๋ถ€ + - ํŽ˜์ด์ง€๋„ค์ด์…˜ + + [ํ•„ํ„ฐ ์˜ต์…˜] + - ์ฐธ์—ฌ ๊ฒฝ๋กœ๋ณ„ ํ•„ํ„ฐ + - ๋‹น์ฒจ ์—ฌ๋ถ€ ํ•„ํ„ฐ (์ „์ฒด/๋‹น์ฒจ/๋ฏธ๋‹น์ฒจ) + + [๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ] + - ์ด๋ฆ„ ๋˜๋Š” ์ „ํ™”๋ฒˆํ˜ธ๋กœ ๊ฒ€์ƒ‰ + + [์•ก์…˜] + - ๊ฐœ๋ณ„ ์ฐธ์—ฌ์ž ์ƒ์„ธ ์ •๋ณด ๋ณด๊ธฐ + +- S/8 + +--- + +UFR-PART-030: [๋‹น์ฒจ์ž์ถ”์ฒจ] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ๊ณต์ •ํ•œ ๋‹น์ฒจ์ž ์„ ์ •์„ ์œ„ํ•ด | ์ž๋™ ์ถ”์ฒจ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ๋‹น์ฒจ์ž ์ž๋™ ์ถ”์ฒจ + ์ด๋ฒคํŠธ ์ข…๋ฃŒ์ผ์ด ๋„๋ž˜ํ•œ ์ƒํ™ฉ์—์„œ | ์†Œ์ƒ๊ณต์ธ์ด "๋‹น์ฒจ์ž ์ถ”์ฒจ" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด | ์‹œ์Šคํ…œ์ด ์ž๋™์œผ๋กœ ๋‹น์ฒจ์ž๋ฅผ ์ถ”์ฒจํ•œ๋‹ค. + + [์ถ”์ฒจ ๋ฐฉ์‹] + - ๋‚œ์ˆ˜ ๊ธฐ๋ฐ˜ ๋ฌด์ž‘์œ„ ์ถ”์ฒจ + - ๋งค์žฅ ๋ฐฉ๋ฌธ ๊ณ ๊ฐ ๊ฐ€์‚ฐ์  ์˜ต์…˜ (์„ ํƒ ๊ฐ€๋Šฅ) + - ์ถ”์ฒจ ๊ณผ์ • ๋กœ๊ทธ ์ž๋™ ๊ธฐ๋ก + + [๋‹น์ฒจ ์ธ์› ์„ค์ •] + - ๊ฒฝํ’ˆ ์ˆ˜๋Ÿ‰ ๊ธฐ๋ฐ˜ ์ž๋™ ์„ค์ • + - ์ˆ˜๋™ ์กฐ์ • ๊ฐ€๋Šฅ + + [์ถ”์ฒจ ๊ฒฐ๊ณผ] + - ๋‹น์ฒจ์ž ๋ชฉ๋ก ํ‘œ์‹œ + - ๋‹น์ฒจ์ž ์ •๋ณด: ์ด๋ฆ„, ์ „ํ™”๋ฒˆํ˜ธ, ์‘๋ชจ๋ฒˆํ˜ธ + + [์žฌ์ถ”์ฒจ] + - "์žฌ์ถ”์ฒจ" ๋ฒ„ํŠผ: ๋‹น์ฒจ์ž ์žฌ์„ ์ • (์ถ”์ฒจ ์ „ ํ™•์ธ) + - ์ด์ „ ์ถ”์ฒจ ์ด๋ ฅ ๋ณด๊ด€ + +- M/13 + +--- + +7. Analytics ์„œ๋น„์Šค +1) ํšจ๊ณผ ์ธก์ • +UFR-ANAL-010: [์„ฑ๊ณผ๋ถ„์„] ์†Œ์ƒ๊ณต์ธ์œผ๋กœ์„œ | ๋‚˜๋Š” ์ด๋ฒคํŠธ ์„ฑ๊ณผ๋ฅผ ํ•œ๋ˆˆ์— ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด | ๋ชจ๋“  ์ง€ํ‘œ๊ฐ€ ํ†ตํ•ฉ๋œ ์‹ค์‹œ๊ฐ„ ์„ฑ๊ณผ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๋ณด๊ณ  ์‹ถ๋‹ค. +- ์‹œ๋‚˜๋ฆฌ์˜ค: ์„ฑ๊ณผ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ์กฐํšŒ + Bottom Navigation์˜ "๋ถ„์„" ํƒญ์„ ํด๋ฆญํ•˜๊ฑฐ๋‚˜, ์ด๋ฒคํŠธ ์ƒ์„ธ ํ™”๋ฉด์—์„œ "์„ฑ๊ณผ๋ถ„์„" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•œ ์ƒํ™ฉ์—์„œ | ์„ฑ๊ณผ๋ถ„์„ ๋Œ€์‹œ๋ณด๋“œ ํ™”๋ฉด์— ์ ‘๊ทผํ•˜๋ฉด | ๋ชจ๋“  ํ•ต์‹ฌ ์ง€ํ‘œ๊ฐ€ ํ•˜๋‚˜์˜ ํ™”๋ฉด์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ‘œ์‹œ๋œ๋‹ค. + + [๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์„ฑ] + + 1. ์ƒ๋‹จ ์š”์•ฝ ์นด๋“œ (4๊ฐœ) + - ์ด ์ฐธ์—ฌ์ž ์ˆ˜ + - ํ˜„์žฌ ์ฐธ์—ฌ์ž ์ˆ˜ + - ๋ชฉํ‘œ ๋Œ€๋น„ ๋‹ฌ์„ฑ๋ฅ  (%) + - ์ด ๋…ธ์ถœ ์ˆ˜ + - ์ฑ„๋„๋ณ„ ๋…ธ์ถœ ํ•ฉ๊ณ„ + - ์ „์ผ ๋Œ€๋น„ ์ฆ๊ฐ๋ฅ  + - ์˜ˆ์ƒ ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  + - ์‹ค์‹œ๊ฐ„ ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  ๊ณ„์‚ฐ + - ์—…์ข… ํ‰๊ท  ๋Œ€๋น„ ๋น„๊ต + - ๋งค์ถœ ์ฆ๊ฐ€์œจ + - POS ์—ฐ๋™ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ + - ์ด๋ฒคํŠธ ์ „ํ›„ ๋น„๊ต (%) + + 2. ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ ๋ถ„์„ (์ฐจํŠธ) + - ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„: ์ฑ„๋„๋ณ„ ์ฐธ์—ฌ์ž ์ˆ˜ + - ์› ๊ทธ๋ž˜ํ”„: ์ฑ„๋„๋ณ„ ์ฐธ์—ฌ ๋น„์œจ + - ์ฑ„๋„ ๋ชฉ๋ก: + - ์šฐ๋ฆฌ๋™๋„คTV + - ๋ง๊ณ ๋น„์ฆˆ + - ์ง€๋‹ˆTV + - Instagram + - Naver Blog + - Kakao Channel + - ๊ฐ ์ฑ„๋„๋ณ„ ์„ธ๋ถ€ ์ง€ํ‘œ: + - ๋…ธ์ถœ ์ˆ˜ + - ์ฐธ์—ฌ์ž ์ˆ˜ + - ์ „ํ™˜์œจ (%) + - ๋น„์šฉ ๋Œ€๋น„ ํšจ์œจ (CPA) + + 3. ์‹œ๊ฐ„๋Œ€๋ณ„ ์ฐธ์—ฌ ์ถ”์ด (๋ผ์ธ ์ฐจํŠธ) + - X์ถ•: ์‹œ๊ฐ„ (์ผ๋ณ„/์‹œ๊ฐ„๋ณ„ ์ „ํ™˜ ๊ฐ€๋Šฅ) + - Y์ถ•: ์ฐธ์—ฌ์ž ์ˆ˜ + - ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (5๋ถ„ ๊ฐ„๊ฒฉ) + - ํ”ผํฌ ์‹œ๊ฐ„๋Œ€ ํ•˜์ด๋ผ์ดํŠธ + + 4. ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  ์ƒ์„ธ ๋ถ„์„ (ํ‘œ) + - ์ด ๋น„์šฉ ์‚ฐ์ถœ + - ๊ฒฝํ’ˆ ๋น„์šฉ + - ์ฑ„๋„๋ณ„ ํ”Œ๋žซํผ ๋น„์šฉ + - ๊ธฐํƒ€ ๋น„์šฉ + - ์˜ˆ์ƒ ์ˆ˜์ต ์‚ฐ์ถœ + - ๋งค์ถœ ์ฆ๊ฐ€์•ก + - ์‹ ๊ทœ ๊ณ ๊ฐ LTV + - ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  ๊ณ„์‚ฐ์‹ ํ‘œ์‹œ + - ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  = (์ˆ˜์ต - ๋น„์šฉ) / ๋น„์šฉ ร— 100 + - ์†์ต๋ถ„๊ธฐ์  ํ‘œ์‹œ + + 5. ์ฐธ์—ฌ์ž ํ”„๋กœํ•„ ๋ถ„์„ (์ฐจํŠธ) + - ์—ฐ๋ น๋Œ€๋ณ„ ๋ถ„ํฌ + - ์„ฑ๋ณ„ ๋ถ„ํฌ + - ์ง€์—ญ๋ณ„ ๋ถ„ํฌ + - ์ฐธ์—ฌ ์‹œ๊ฐ„๋Œ€ ๋ถ„์„ + + 6. ๋น„๊ต ๋ถ„์„ (ํ‘œ) + - ์—…์ข… ํ‰๊ท ๊ณผ ๋น„๊ต + - ์ฐธ์—ฌ์œจ + - ํˆฌ์ž ๋Œ€๋น„ ์ˆ˜์ต๋ฅ  + - ์ „ํ™˜์œจ + - ๋‚ด ์ด์ „ ์ด๋ฒคํŠธ์™€ ๋น„๊ต + - ์„ฑ๊ณผ ๊ฐœ์„ ๋„ + - ์ตœ๊ณ /์ตœ์ € ๊ธฐ๋ก ๋Œ€๋น„ + + [์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ] + - ๋ชจ๋“  ์ง€ํ‘œ 5๋ถ„ ๊ฐ„๊ฒฉ ์ž๋™ ๊ฐฑ์‹  + - ์ƒˆ๋กœ์šด ์ฐธ์—ฌ์ž ์•Œ๋ฆผ (์„ ํƒ ์˜ต์…˜) + - ์ˆ˜๋™ ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ + + [๋ฐ์ดํ„ฐ ์†Œ์Šค] + - Participation ์„œ๋น„์Šค: ์ฐธ์—ฌ์ž ๋ฐ์ดํ„ฐ + - Distribution ์„œ๋น„์Šค: ์ฑ„๋„๋ณ„ ๋…ธ์ถœ ์ˆ˜ + - POS ์‹œ์Šคํ…œ: ๋งค์ถœ ๋ฐ์ดํ„ฐ (์—ฐ๋™ ์‹œ) + - ์™ธ๋ถ€ API: ์šฐ๋ฆฌ๋™๋„คTV, ์ง€๋‹ˆTV, SNS ํ†ต๊ณ„ + + [๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”] + - ๋ฐ˜์‘ํ˜• ๋””์ž์ธ + - ์Šคํฌ๋กค ๊ฐ€๋Šฅํ•œ ๋ ˆ์ด์•„์›ƒ + - ํ•ต์‹ฌ ์ง€ํ‘œ ์šฐ์„  ํ‘œ์‹œ + +- M/34 + +- ๊ธฐ์ˆ  ํƒœ์Šคํฌ + - ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ + - 5๋ถ„ ๊ฐ„๊ฒฉ ๋ฐ์ดํ„ฐ ํด๋ง + - WebSocket ๋˜๋Š” SSE๋ฅผ ํ†ตํ•œ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ + - ๋ฐ์ดํ„ฐ ์บ์‹ฑ (Redis) + - ๋‹ค์ค‘ API ํ†ตํ•ฉ + - ์šฐ๋ฆฌ๋™๋„คTV API + - ๋ง๊ณ ๋น„์ฆˆ API + - ์ง€๋‹ˆTV API + - Instagram API + - Naver API + - Kakao API + - POS ์‹œ์Šคํ…œ ์—ฐ๋™ + - ๋งค์ถœ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ + - ์ด๋ฒคํŠธ ์ „ํ›„ ๋น„๊ต ๋กœ์ง + - ์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ™œ์šฉ + - Chart.js / Recharts ๋“ฑ + - ๋ฐ˜์‘ํ˜• ์ฐจํŠธ ๊ตฌํ˜„ + - ์„ฑ๋Šฅ ์ตœ์ ํ™” + - ์ฐจํŠธ ๋ฐ์ดํ„ฐ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ + - ๋ฌดํ•œ ์Šคํฌ๋กค ๋Œ€์‹  ํŽ˜์ด์ง€๋„ค์ด์…˜ + - ์ด๋ฏธ์ง€/๋ฐ์ดํ„ฐ lazy loading + +--- + +## ๊ธฐ์ˆ  ๊ฒ€ํ†  ๊ฒฐ๊ณผ +**[๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋ณ„ ๊ฒ€ํ† ]** + +1) User ์„œ๋น„์Šค +๋ชจ๋“  ์œ ์ €์Šคํ† ๋ฆฌ **โœ… ์‹คํ˜„ ๊ฐ€๋Šฅ** +- ํ‘œ์ค€์ ์ธ ์ธ์ฆ/์ธ๊ฐ€ ํŒจํ„ด +- JWT ํ† ํฐ, Redis ์บ์‹ฑ ๋“ฑ ๊ฒ€์ฆ๋œ ๊ธฐ์ˆ  ํ™œ์šฉ +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ ๊ฒ€์ฆ API (๊ตญ์„ธ์ฒญ) ์—ฐ๋™ + +2) Event ์„œ๋น„์Šค +๋Œ€๋ถ€๋ถ„ **โœ… ์‹คํ˜„ ๊ฐ€๋Šฅ** +- ์ด๋ฒคํŠธ CRUD ๊ธฐ๋Šฅ +- AI ์„œ๋น„์Šค์™€์˜ ์—ฐ๋™ +- LocalStorage ์ž๋™ ์ €์žฅ ๊ตฌํ˜„ + +3) AI ์„œ๋น„์Šค +**โš ๏ธ ๋†’์€ ๊ธฐ์ˆ ์  ๋ณต์žก๋„** + +- UFR-AI-010: **์ฃผ์š” ๋ฆฌ์Šคํฌ ํฌ์ธํŠธ** + - Claude API ๋˜๋Š” GPT-4 API ์—ฐ๋™ + - ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ด๋ฒคํŠธ ์ถ”์ฒœ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ + - ์‘๋‹ต ์‹œ๊ฐ„ ๊ด€๋ฆฌ (10์ดˆ ๋ชฉํ‘œ) + - ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง ์ตœ์ ํ™” + - **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: ์บ์‹ฑ, ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ, ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ + +4) Content ์„œ๋น„์Šค +**โš ๏ธ ์ค‘๊ฐ„ ๋ณต์žก๋„** + +- UFR-CONT-010: **์ด๋ฏธ์ง€ ์ƒ์„ฑ API ์—ฐ๋™** + - Stable Diffusion ๋˜๋Š” DALL-E API + - ํ”Œ๋žซํผ๋ณ„ ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์ตœ์ ํ™” + - **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: ์ด๋ฏธ์ง€ ์บ์‹ฑ, CDN ํ™œ์šฉ + +5) Distribution ์„œ๋น„์Šค +**โš ๏ธ ๋‹ค์ค‘ API ํ†ตํ•ฉ ๋ณต์žก๋„** + +- UFR-DIST-010: **์™ธ๋ถ€ API ๋‹ค์ค‘ ์—ฐ๋™** + - ์šฐ๋ฆฌ๋™๋„คTV API + - ๋ง๊ณ ๋น„์ฆˆ API + - ์ง€๋‹ˆTV API + - SNS API (Instagram, Naver, Kakao) + - **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: ์ฑ„๋„๋ณ„ ๋…๋ฆฝ ์ฒ˜๋ฆฌ, ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„, ์จํ‚ท ๋ธŒ๋ ˆ์ด์ปค ํŒจํ„ด + +6) Participation ์„œ๋น„์Šค +๋Œ€๋ถ€๋ถ„ **โœ… ์‹คํ˜„ ๊ฐ€๋Šฅ** +- ํ‘œ์ค€์ ์ธ CRUD ๊ธฐ๋Šฅ +- ์ถ”์ฒจ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ + +7) Analytics ์„œ๋น„์Šค +**โš ๏ธ ๋†’์€ ํ†ตํ•ฉ ๋ณต์žก๋„** + +- UFR-ANAL-010: **ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„** + - ๋‹ค์ค‘ ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ†ตํ•ฉ + - ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๋ฐ ํ‘œ์‹œ + - ๋ณต์žกํ•œ ์ฐจํŠธ ๋ฐ ๊ทธ๋ž˜ํ”„ + - **ํ•ด๊ฒฐ๋ฐฉ์•ˆ**: ๋ฐ์ดํ„ฐ ์บ์‹ฑ (Redis), WebSocket/SSE, ์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ™œ์šฉ + +**[์ฃผ์š” ๊ธฐ์ˆ  ์Šคํƒ ๊ถŒ์žฅ์‚ฌํ•ญ]** + +**๋ฐฑ์—”๋“œ:** +- Node.js/Spring Boot (๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค) +- Redis (์บ์‹ฑ, ์„ธ์…˜, ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ) +- PostgreSQL/MongoDB +- RabbitMQ/Kafka (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) + +**ํ”„๋ก ํŠธ์—”๋“œ:** +- React/Next.js +- TypeScript +- Zustand/Redux (์ƒํƒœ๊ด€๋ฆฌ) +- React Query (API ์ƒํƒœ๊ด€๋ฆฌ) +- Chart.js/Recharts (์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ) + +**์ธํ”„๋ผ:** +- Docker/Kubernetes +- API Gateway +- Circuit Breaker +- ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ + +**์™ธ๋ถ€ API:** +- AI: Claude API / GPT-4 API +- ์ด๋ฏธ์ง€ ์ƒ์„ฑ: Stable Diffusion / DALL-E +- ์‚ฌ์—…์ž ๊ฒ€์ฆ: ๊ตญ์„ธ์ฒญ API +- ๋ฐฐํฌ ์ฑ„๋„: ์šฐ๋ฆฌ๋™๋„คTV, ๋ง๊ณ ๋น„์ฆˆ, ์ง€๋‹ˆTV, SNS APIs + +**[์ฃผ์š” ๊ธฐ์ˆ ์  ๋ฆฌ์Šคํฌ ๋ฐ ํ•ด๊ฒฐ๋ฐฉ์•ˆ]** + +**1. AI ์‘๋‹ต ์‹œ๊ฐ„ (10์ดˆ ๋ชฉํ‘œ)** +- ๋ฆฌ์Šคํฌ: AI API ์‘๋‹ต์ด ๋ชฉํ‘œ ์‹œ๊ฐ„์„ ์ดˆ๊ณผํ•  ์ˆ˜ ์žˆ์Œ +- ํ•ด๊ฒฐ๋ฐฉ์•ˆ: + - ํ”„๋กฌํ”„ํŠธ ์ตœ์ ํ™”๋กœ ์‘๋‹ต ์‹œ๊ฐ„ ๋‹จ์ถ• + - ํŠธ๋ Œ๋“œ ๋ถ„์„๊ณผ ์ด๋ฒคํŠธ ์ถ”์ฒœ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ + - ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ + ํด๋ง ๋ฐฉ์‹ + - ๋ถ€๋ถ„ ์‘๋‹ต ์ŠคํŠธ๋ฆฌ๋ฐ + +**2. ๋‹ค์ค‘ ์™ธ๋ถ€ API ์˜์กด์„ฑ** +- ๋ฆฌ์Šคํฌ: API ์žฅ์•  ์‹œ ์„œ๋น„์Šค ์ค‘๋‹จ +- ํ•ด๊ฒฐ๋ฐฉ์•ˆ: + - ๊ฐ API๋ณ„ ํด๋ฐฑ ์ „๋žต ์ˆ˜๋ฆฝ + - ์บ์‹ฑ ์ ๊ทน ํ™œ์šฉ + - ์จํ‚ท ๋ธŒ๋ ˆ์ด์ปค ํŒจํ„ด ์ ์šฉ + - ์ฑ„๋„๋ณ„ ๋…๋ฆฝ ์ฒ˜๋ฆฌ + +**3. ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ์„ฑ๋Šฅ** +- ๋ฆฌ์Šคํฌ: ๋‹ค์ค‘ ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ†ตํ•ฉ์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์ €ํ•˜ +- ํ•ด๊ฒฐ๋ฐฉ์•ˆ: + - Redis ์บ์‹ฑ + - 5๋ถ„ ๊ฐ„๊ฒฉ ๋ฐ์ดํ„ฐ ํด๋ง + - WebSocket/SSE๋ฅผ ํ†ตํ•œ ํšจ์œจ์ ์ธ ์—…๋ฐ์ดํŠธ + - ์ฐจํŠธ ๋ฐ์ดํ„ฐ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ + +**4. ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋น„์šฉ ๋ฐ ์‹œ๊ฐ„** +- ๋ฆฌ์Šคํฌ: ์ด๋ฏธ์ง€ ์ƒ์„ฑ API ๋น„์šฉ ๋ฐ ์ƒ์„ฑ ์‹œ๊ฐ„ +- ํ•ด๊ฒฐ๋ฐฉ์•ˆ: + - ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ์บ์‹ฑ + - CDN ํ™œ์šฉ + - ๋น„๋™๊ธฐ ์ƒ์„ฑ + ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ + +**[๊ฐ„์†Œํ™”๋œ ํ”Œ๋กœ์šฐ ๊ฒ€์ฆ]** + +โœ… **์š”์ฒญ์‚ฌํ•ญ ๋ฐ˜์˜ ํ™•์ธ** +1. โœ… KT์ธ์ฆ์‹œ์Šคํ…œ์—ฐ๋™ ์ œ์™ธ๋จ +2. โœ… ์‹ ๊ทœ๊ฐ€์ž… ์‹œ ๋ฌด๋ฃŒ์ฒดํ—˜ ๊ธฐ๋Šฅ ์‚ญ์ œ๋จ +3. โœ… 5ํšŒ์—ฐ์†์‹คํŒจ์‹œ ๊ณ„์ •์ž ๊ธˆ๊ธฐ๋Šฅ ์‚ญ์ œ๋จ +4. โœ… ์ด๋ฒคํŠธ์ปค์Šคํ…€ ํ™”๋ฉด ํ†ตํ•ฉ๋จ (์ด๋ฒคํŠธ์ถ”์ฒœ์กฐํšŒ ํ™”๋ฉด์—์„œ ๊ฐ„๋‹จํžˆ ์ œ๋ชฉ/๊ฒฝํ’ˆ ์ˆ˜์ • ๊ฐ€๋Šฅ) +5. โœ… UFR-AI-010๊ณผ UFR-AI-020 ํ†ตํ•ฉ๋จ (ํŠธ๋ Œ๋“œ ๋ถ„์„ + ์ด๋ฒคํŠธ ์ถ”์ฒœ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌ) +6. โœ… UFR-PART-040 ์‚ญ์ œ๋จ (๋‹น์ฒจ์•Œ๋ฆผ๋ฐœ์†ก) +7. โœ… UFR-ANAL-020 ์‚ญ์ œ๋จ (๋ถ„์„๋ฆฌํฌํŠธ์ƒ์„ฑ) +8. โœ… PDF / Excel ๋‚ด๋ณด๋‚ด๊ธฐ/๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ ์‚ญ์ œ๋จ + +**[์ด๋ฒคํŠธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ (์ตœ์ข…)]** +1. ์ด๋ฒคํŠธ ๋ชฉ์  ์„ ํƒ (07-์ด๋ฒคํŠธ๋ชฉ์ ์„ ํƒ) +2. AI ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ (08-AI์ด๋ฒคํŠธ์ถ”์ฒœ) + - 3๊ฐ€์ง€ ์ถ”์ฒœ, ์ œ๋ชฉ/๊ฒฝํ’ˆ ๊ฐ„๋‹จ ์ˆ˜์ • ๊ฐ€๋Šฅ +3. SNS ์ด๋ฏธ์ง€ ์ƒ์„ฑ (09-SNS์ด๋ฏธ์ง€์ƒ์„ฑ) + - 3๊ฐ€์ง€ ์Šคํƒ€์ผ ์นด๋“œ ์ค‘ ์„ ํƒ +4. ์ฝ˜ํ…์ธ  ํŽธ์ง‘ (10-์ฝ˜ํ…์ธ ํŽธ์ง‘) + - ํ…์ŠคํŠธ, ์ƒ‰์ƒ ํŽธ์ง‘ +5. ๋ฐฐํฌ์ฑ„๋„ ์„ ํƒ (11-๋ฐฐํฌ์ฑ„๋„์„ ํƒ) +6. ์ตœ์ข…์Šน์ธ (12-์ตœ์ข…์Šน์ธ) + +**[์„œ๋น„์Šค ๊ฐ„ ์˜์กด์„ฑ]** +- User โ†’ Event: ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด ์ œ๊ณต +- Event โ†’ AI: ํŠธ๋ Œ๋“œ ๋ถ„์„ ๋ฐ ์ด๋ฒคํŠธ ์ถ”์ฒœ ์š”์ฒญ +- Event โ†’ Content: ์ฝ˜ํ…์ธ  ์ƒ์„ฑ ์š”์ฒญ +- Event โ†’ Distribution: ๋ฐฐํฌ ์š”์ฒญ +- Distribution โ†’ Content: ์ƒ์„ฑ๋œ ์ฝ˜ํ…์ธ  ์‚ฌ์šฉ +- Participation โ†’ Analytics: ์ฐธ์—ฌ์ž ๋ฐ์ดํ„ฐ ์ œ๊ณต +- Distribution โ†’ Analytics: ๋…ธ์ถœ ๋ฐ์ดํ„ฐ ์ œ๊ณต + +**[์œ ์ €์Šคํ† ๋ฆฌ ํ†ต๊ณ„]** +- User ์„œ๋น„์Šค: 4๊ฐœ +- Event ์„œ๋น„์Šค: 7๊ฐœ +- AI ์„œ๋น„์Šค: 1๊ฐœ (ํ†ตํ•ฉ) +- Content ์„œ๋น„์Šค: 2๊ฐœ +- Distribution ์„œ๋น„์Šค: 2๊ฐœ +- Participation ์„œ๋น„์Šค: 3๊ฐœ +- Analytics ์„œ๋น„์Šค: 1๊ฐœ + +**์ด 20๊ฐœ ์œ ์ €์Šคํ† ๋ฆฌ** + +**[์˜ˆ์ƒ ๊ฐœ๋ฐœ ๊ธฐ๊ฐ„]** +- User ์„œ๋น„์Šค: 2์ฃผ +- Event ์„œ๋น„์Šค: 3์ฃผ +- AI ์„œ๋น„์Šค: 4์ฃผ (ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง ํฌํ•จ) +- Content ์„œ๋น„์Šค: 3์ฃผ +- Distribution ์„œ๋น„์Šค: 4์ฃผ (๋‹ค์ค‘ API ์—ฐ๋™) +- Participation ์„œ๋น„์Šค: 2์ฃผ +- Analytics ์„œ๋น„์Šค: 4์ฃผ (ํ†ตํ•ฉ ๋Œ€์‹œ๋ณด๋“œ) + +**์ด ์˜ˆ์ƒ ๊ธฐ๊ฐ„**: 12-16์ฃผ (๋ณ‘๋ ฌ ๊ฐœ๋ฐœ ์‹œ) +``` diff --git a/design/๊ตฌํ˜„๋ฐฉ์•ˆ-AI์ด๋ฒคํŠธ์„ค๊ณ„.md b/design/๊ตฌํ˜„๋ฐฉ์•ˆ-AI์ด๋ฒคํŠธ์„ค๊ณ„.md new file mode 100644 index 0000000..44f394d --- /dev/null +++ b/design/๊ตฌํ˜„๋ฐฉ์•ˆ-AI์ด๋ฒคํŠธ์„ค๊ณ„.md @@ -0,0 +1,1612 @@ +# AI ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ์ถ”์ฒœ ์‹œ์Šคํ…œ ๊ตฌํ˜„๋ฐฉ์•ˆ + +**์ž‘์„ฑ์ผ**: 2025-10-21 +**๋ฒ„์ „**: 1.0 +**์ž‘์„ฑ์ž**: ํ”„๋กœ์ ํŠธ ํŒ€ ์ „์ฒด + +--- + +## ๋ชฉ์ฐจ +1. [๊ฐœ์š”](#๊ฐœ์š”) +2. [๋ฐ์ดํ„ฐ ํ™•๋ณด ๋ฐ ์ฒ˜๋ฆฌ ๋ฐฉ์•ˆ](#๋ฐ์ดํ„ฐ-ํ™•๋ณด-๋ฐ-์ฒ˜๋ฆฌ-๋ฐฉ์•ˆ) +3. [Claude API ์—ฐ๋™ ๊ตฌ์กฐ](#claude-api-์—ฐ๋™-๊ตฌ์กฐ) +4. [์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜](#์‹œ์Šคํ…œ-์•„ํ‚คํ…์ฒ˜) +5. [์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต](#์„ฑ๋Šฅ-์ตœ์ ํ™”-์ „๋žต) +6. [๊ตฌํ˜„ ๋กœ๋“œ๋งต](#๊ตฌํ˜„-๋กœ๋“œ๋งต) + +--- + +## ๊ฐœ์š” + +### ๋ชฉ์  +์†Œ์ƒ๊ณต์ธ์ด ์ด๋ฒคํŠธ ๋ชฉ์ ์„ ์„ ํƒํ•˜๋ฉด, AI๊ฐ€ ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ๋ฅผ ๋ถ„์„ํ•˜๊ณ  3๊ฐ€์ง€ ์˜ˆ์‚ฐ๋ณ„ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ(๊ฐ ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ 2๊ฐœ์”ฉ ์ด 6๊ฐœ)์„ ์ถ”์ฒœํ•˜๋Š” ์‹œ์Šคํ…œ ๊ตฌํ˜„ + +### ํ•ต์‹ฌ ์š”๊ตฌ์‚ฌํ•ญ +- **์‘๋‹ต ์‹œ๊ฐ„**: 10์ดˆ ์ด๋‚ด +- **์ถ”์ฒœ ๊ฐœ์ˆ˜**: 6๊ฐœ (์ €/์ค‘/๊ณ  ์˜ˆ์‚ฐ ร— ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ) +- **ํฌํ•จ ์ •๋ณด**: ํŠธ๋ Œ๋“œ ๋ถ„์„, ์ด๋ฒคํŠธ ์ œ๋ชฉ, ๊ฒฝํ’ˆ, ์ฐธ์—ฌ๋ฐฉ๋ฒ•, ์˜ˆ์ƒ ์ฐธ์—ฌ์ž, ๋น„์šฉ, ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  + +### ๊ธฐ์ˆ  ์Šคํƒ ๊ฒฐ์ • +- **AI ๋ชจ๋ธ**: Claude 3.5 Sonnet API +- **๋ฒกํ„ฐ DB**: Pinecone (๊ด€๋ฆฌํ˜• ์„œ๋น„์Šค) +- **์ž„๋ฒ ๋”ฉ**: OpenAI text-embedding-3-large +- **์บ์‹ฑ**: Redis Cluster +- **๋ฐฑ์—”๋“œ**: Node.js (๋˜๋Š” Spring Boot) +- **๋ฉ”์‹œ์ง€ ํ**: RabbitMQ (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) + +--- + +## ๋ฐ์ดํ„ฐ ํ™•๋ณด ๋ฐ ์ฒ˜๋ฆฌ ๋ฐฉ์•ˆ + +### 1. ๋ฐ์ดํ„ฐ ์†Œ์Šค + +#### 1.1 ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ (์ดˆ๊ธฐ ํ•™์Šต์šฉ) + +**๊ณต๊ณต๋ฐ์ดํ„ฐ** +- ์†Œ์ƒ๊ณต์ธ์ง„ํฅ๊ณต๋‹จ API + - ์—…์ข…๋ณ„ ์‚ฌ์—…์ฒด ์ˆ˜ + - ์ง€์—ญ๋ณ„ ๋งค์ถœ ํ†ต๊ณ„ + - ์‹œ์ฆŒ๋ณ„ ์†Œ๋น„ ํŠธ๋ Œ๋“œ +- ํ†ต๊ณ„์ฒญ ๋ฐ์ดํ„ฐ + - ์—…์ข…๋ณ„ ์›”๋ณ„ ๋งค์ถœ์•ก + - ์ง€์—ญ๋ณ„ ์†Œ๋น„์ž ํŠน์„ฑ + - ์—ฐ๋ น๋Œ€๋ณ„ ์†Œ๋น„ ํŒจํ„ด + +**SNS ๋ฐ ๋ธ”๋กœ๊ทธ ๋ฐ์ดํ„ฐ** +- ๋„ค์ด๋ฒ„ ๋ธ”๋กœ๊ทธ ๊ฒ€์ƒ‰ API + - ํ‚ค์›Œ๋“œ: "์†Œ์ƒ๊ณต์ธ ์ด๋ฒคํŠธ", "๋งค์žฅ ํ”„๋กœ๋ชจ์…˜" + - ์ˆ˜์ง‘ ํ•ญ๋ชฉ: ์ด๋ฒคํŠธ๋ช…, ๊ฒฝํ’ˆ, ์ฐธ์—ฌ๋ฐฉ๋ฒ•, ํ›„๊ธฐ +- Instagram Graph API + - ํ•ด์‹œํƒœ๊ทธ: #์†Œ์ƒ๊ณต์ธ์ด๋ฒคํŠธ, #๊ฐ€๊ฒŒ์ด๋ฒคํŠธ + - ์ˆ˜์ง‘ ํ•ญ๋ชฉ: ์ด๋ฏธ์ง€, ์บก์…˜, ์ข‹์•„์š”/๋Œ“๊ธ€ ์ˆ˜ + +**๊ฒฝ์Ÿ์‚ฌ/๋ฒค์น˜๋งˆํฌ ๋ฐ์ดํ„ฐ** +- ์œ ์‚ฌ ์„œ๋น„์Šค ๊ณต๊ฐœ ์‚ฌ๋ก€ ๋ถ„์„ +- ์„ฑ๊ณต ์‚ฌ๋ก€ DB ๊ตฌ์ถ• (100~500๊ฑด) + +#### 1.2 ์ž์‚ฌ ๋ฐ์ดํ„ฐ (์šด์˜ ๋ฐ์ดํ„ฐ) + +**์‚ฌ์šฉ์ž ํ”„๋กœํ•„** +- ๋งค์žฅ๋ช…, ์—…์ข…, ์ฃผ์†Œ, ์˜์—…์‹œ๊ฐ„ +- ์‚ฌ์—…์ž๋ฒˆํ˜ธ (์—…์ข… ๋ถ„๋ฅ˜ ์šฉ) + +**์ด๋ฒคํŠธ ์ƒ์„ฑ ๋ฐ์ดํ„ฐ** +- ์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑํ•œ ์ด๋ฒคํŠธ ์ •๋ณด +- AI ์ถ”์ฒœ ์ค‘ ์„ ํƒํ•œ ์˜ต์…˜ +- ์ปค์Šคํ…€ ์ˆ˜์ • ๋‚ด์šฉ (์ œ๋ชฉ, ๊ฒฝํ’ˆ ๋ณ€๊ฒฝ) + +**์ด๋ฒคํŠธ ์„ฑ๊ณผ ๋ฐ์ดํ„ฐ** +- ์ฐธ์—ฌ์ž ์ˆ˜ +- ์‹ค์ œ ๋น„์šฉ +- ์‹ค์ œ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  +- ๋ฐฐํฌ ์ฑ„๋„๋ณ„ ์„ฑ๊ณผ + +**์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ** +- "๋‹ค์‹œ ์ถ”์ฒœ๋ฐ›๊ธฐ" ํด๋ฆญ (๋ถ€์ •์  ํ”ผ๋“œ๋ฐฑ) +- ์ด๋ฒคํŠธ ์„ ํƒ (๊ธ์ •์  ํ”ผ๋“œ๋ฐฑ) +- ์ถ”์ฒœ๊ณผ ์‹ค์ œ ์„ฑ๊ณผ ์ฐจ์ด + +### 2. ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ”„๋กœ์„ธ์Šค + +#### 2.1 ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ (ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ ์‹œ) + +```python +# ETL ํŒŒ์ดํ”„๋ผ์ธ (Apache Airflow DAG) + +# ์ผ์ผ ๋ฐฐ์น˜ ์ž‘์—… +@dag(schedule_interval='0 2 * * *') # ๋งค์ผ ์ƒˆ๋ฒฝ 2์‹œ +def collect_external_data(): + + # Task 1: ๊ณต๊ณต๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ + @task + def fetch_public_data(): + # ์†Œ์ƒ๊ณต์ธ์ง„ํฅ๊ณต๋‹จ API ํ˜ธ์ถœ + # ํ†ต๊ณ„์ฒญ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ + return data + + # Task 2: SNS ํฌ๋กค๋ง + @task + def crawl_sns_data(): + # ๋„ค์ด๋ฒ„ ๋ธ”๋กœ๊ทธ ๊ฒ€์ƒ‰ + # Instagram ํ•ด์‹œํƒœ๊ทธ ๊ฒ€์ƒ‰ + return data + + # Task 3: ๋ฐ์ดํ„ฐ ์ •์ œ + @task + def clean_data(raw_data): + # ์ค‘๋ณต ์ œ๊ฑฐ + # ์ด์ƒ์น˜ ํƒ์ง€ ๋ฐ ์ œ๊ฑฐ + # ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํƒœ๊น… + # ํ…์ŠคํŠธ ์ •๊ทœํ™” + return cleaned_data + + # Task 4: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ + @task + def save_to_db(cleaned_data): + # PostgreSQL์— ์ €์žฅ + # events ํ…Œ์ด๋ธ”์— insert + pass + + # Task 5: ๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ + @task + def generate_embeddings(cleaned_data): + # OpenAI Embeddings API ํ˜ธ์ถœ + # Pinecone์— ์ €์žฅ + pass +``` + +#### 2.2 ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ + +```javascript +// ์ด๋ฒคํŠธ ์ƒ์„ฑ ์‹œ +async function onEventCreated(eventData) { + // 1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ €์žฅ + await db.events.create(eventData); + + // 2. ๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ (๋น„๋™๊ธฐ) + await queue.publish('embedding', { + eventId: eventData.id, + text: `${eventData.title} ${eventData.prize} ${eventData.participation}` + }); +} + +// ์ด๋ฒคํŠธ ์„ฑ๊ณผ ์ˆ˜์ง‘ +async function onEventCompleted(eventId, performanceData) { + // ์„ฑ๊ณผ ๋ฐ์ดํ„ฐ ์ €์žฅ + await db.eventPerformance.create({ + eventId, + actualParticipants: performanceData.participants, + actualCost: performanceData.cost, + actualRoi: performanceData.roi + }); + + // ์ถ”์ฒœ ์ •ํ™•๋„ ๊ณ„์‚ฐ + const prediction = await db.eventRecommendations.findOne({ eventId }); + const accuracy = calculateAccuracy(prediction, performanceData); + + // ๋ชจ๋ธ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง + await logAccuracy(accuracy); +} +``` + +### 3. ๋ฐ์ดํ„ฐ ์ •์ œ ํ”„๋กœ์„ธ์Šค + +#### 3.1 ๋ฐ์ดํ„ฐ ์ •์ œ ๊ทœ์น™ + +**ํ…์ŠคํŠธ ์ •๊ทœํ™”** +```python +def normalize_event_data(raw_event): + return { + 'title': clean_text(raw_event['title']), # ํŠน์ˆ˜๋ฌธ์ž ์ œ๊ฑฐ, ์†Œ๋ฌธ์ž ๋ณ€ํ™˜ + 'prize': normalize_prize_name(raw_event['prize']), # ๊ฒฝํ’ˆ๋ช… ํ‘œ์ค€ํ™” + 'participation': normalize_participation(raw_event['participation']), + 'industry': classify_industry(raw_event['industry']), # ์—…์ข… ๋ถ„๋ฅ˜ + 'location': parse_location(raw_event['location']), # ์ง€์—ญ ํŒŒ์‹ฑ + 'season': extract_season(raw_event['date']), # ์‹œ์ฆŒ ์ถ”์ถœ + 'cost': parse_cost(raw_event['cost']), # ๋น„์šฉ ์ˆซ์ž ๋ณ€ํ™˜ + 'roi': parse_roi(raw_event['roi']) # ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  ์ˆซ์ž ๋ณ€ํ™˜ + } +``` + +**์—…์ข… ๋ถ„๋ฅ˜ ํ‘œ์ค€ํ™”** +```python +INDUSTRY_MAPPING = { + '์Œ์‹์ ': ['ํ•œ์‹', '์ค‘์‹', '์ผ์‹', '์–‘์‹', '์นดํŽ˜', '๋ฒ ์ด์ปค๋ฆฌ', '์น˜ํ‚จ', 'ํ”ผ์ž'], + '์†Œ๋งค์ ': ['ํŽธ์˜์ ', '์Šˆํผ๋งˆ์ผ“', 'ํ™”์žฅํ’ˆ', '์˜๋ฅ˜', '์žกํ™”'], + '์„œ๋น„์Šค': ['๋ฏธ์šฉ์‹ค', '๋„ค์ผ์ƒต', 'PC๋ฐฉ', '๋…ธ๋ž˜๋ฐฉ', 'ํ—ฌ์Šค์žฅ'], + '์ˆ™๋ฐ•': ['๋ชจํ…”', 'ํ˜ธํ…”', '๊ฒŒ์ŠคํŠธํ•˜์šฐ์Šค', 'ํŽœ์…˜'] +} + +def classify_industry(raw_industry): + for category, subcategories in INDUSTRY_MAPPING.items(): + if raw_industry in subcategories: + return category + return '๊ธฐํƒ€' +``` + +**์ง€์—ญ ํŒŒ์‹ฑ** +```python +def parse_location(address): + # "์„œ์šธํŠน๋ณ„์‹œ ๊ฐ•๋‚จ๊ตฌ ์—ญ์‚ผ๋™" -> {"city": "์„œ์šธ", "district": "๊ฐ•๋‚จ๊ตฌ"} + import re + + city_pattern = r'(์„œ์šธ|๋ถ€์‚ฐ|๋Œ€๊ตฌ|์ธ์ฒœ|๊ด‘์ฃผ|๋Œ€์ „|์šธ์‚ฐ|์„ธ์ข…|๊ฒฝ๊ธฐ|๊ฐ•์›|์ถฉ๋ถ|์ถฉ๋‚จ|์ „๋ถ|์ „๋‚จ|๊ฒฝ๋ถ|๊ฒฝ๋‚จ|์ œ์ฃผ)' + district_pattern = r'([๊ฐ€-ํžฃ]+๊ตฌ)' + + city = re.search(city_pattern, address) + district = re.search(district_pattern, address) + + return { + 'city': city.group(1) if city else None, + 'district': district.group(1) if district else None + } +``` + +**์‹œ์ฆŒ ์ถ”์ถœ** +```python +def extract_season(date): + month = date.month + if month in [12, 1, 2]: + return '๊ฒจ์šธ' + elif month in [3, 4, 5]: + return '๋ด„' + elif month in [6, 7, 8]: + return '์—ฌ๋ฆ„' + else: + return '๊ฐ€์„' +``` + +#### 3.2 ์ด์ƒ์น˜ ํƒ์ง€ + +```python +def detect_outliers(events_df): + # IQR ๋ฐฉ์‹์œผ๋กœ ์ด์ƒ์น˜ ํƒ์ง€ + Q1 = events_df['roi'].quantile(0.25) + Q3 = events_df['roi'].quantile(0.75) + IQR = Q3 - Q1 + + lower_bound = Q1 - 1.5 * IQR + upper_bound = Q3 + 1.5 * IQR + + # ์ด์ƒ์น˜ ์ œ๊ฑฐ + filtered_df = events_df[ + (events_df['roi'] >= lower_bound) & + (events_df['roi'] <= upper_bound) + ] + + return filtered_df +``` + +### 4. ๋ฒกํ„ฐ๋ผ์ด์ง• ์ „๋žต + +#### 4.1 ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ + +```python +import openai +import pinecone + +# OpenAI ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ +def generate_embedding(text): + response = openai.embeddings.create( + model="text-embedding-3-large", # 3072 ์ฐจ์› + input=text + ) + return response.data[0].embedding + +# ์ด๋ฒคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ +def event_to_text(event): + return f""" +์ด๋ฒคํŠธ๋ช…: {event['title']} +์—…์ข…: {event['industry']} +๊ฒฝํ’ˆ: {event['prize']} +์ฐธ์—ฌ๋ฐฉ๋ฒ•: {event['participation']} +์ง€์—ญ: {event['location']['city']} {event['location']['district']} +์‹œ์ฆŒ: {event['season']} +์˜ˆ์‚ฐ: {event['cost']}์› +ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ : {event['roi']}% +""" + +# Pinecone์— ์ €์žฅ +def save_to_pinecone(event): + text = event_to_text(event) + embedding = generate_embedding(text) + + pinecone_index.upsert(vectors=[{ + 'id': event['id'], + 'values': embedding, + 'metadata': { + 'title': event['title'], + 'industry': event['industry'], + 'location': event['location']['district'], + 'season': event['season'], + 'budget': event['cost'], + 'roi': event['roi'], + 'prize': event['prize'], + 'participation': event['participation'] + } + }]) +``` + +#### 4.2 ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ + +```python +# ์œ ์‚ฌ ์ด๋ฒคํŠธ ๊ฒ€์ƒ‰ +def search_similar_events(query_event, top_k=5): + # ์ฟผ๋ฆฌ ํ…์ŠคํŠธ ์ƒ์„ฑ + query_text = event_to_text(query_event) + + # ์ฟผ๋ฆฌ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ + query_embedding = generate_embedding(query_text) + + # ํ•„ํ„ฐ ์กฐ๊ฑด ๊ตฌ์„ฑ + filters = { + 'industry': query_event['industry'], + 'budget': {'$gte': query_event['cost'] * 0.5, '$lte': query_event['cost'] * 1.5} + } + + # Pinecone ๊ฒ€์ƒ‰ + results = pinecone_index.query( + vector=query_embedding, + filter=filters, + top_k=top_k, + include_metadata=True + ) + + return results['matches'] +``` + +#### 4.3 Pinecone ์ธ๋ฑ์Šค ์„ค์ • + +```python +import pinecone + +# Pinecone ์ดˆ๊ธฐํ™” +pinecone.init( + api_key="YOUR_API_KEY", + environment="us-west1-gcp" +) + +# ์ธ๋ฑ์Šค ์ƒ์„ฑ +index_name = "kt-event-recommendations" + +if index_name not in pinecone.list_indexes(): + pinecone.create_index( + name=index_name, + dimension=3072, # text-embedding-3-large ์ฐจ์› + metric='cosine', + pods=1, + pod_type='p1.x1' # ์„ฑ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์กฐ์ • + ) + +# ์ธ๋ฑ์Šค ์—ฐ๊ฒฐ +pinecone_index = pinecone.Index(index_name) +``` + +### 5. ๋ฐ์ดํ„ฐ ํ†ต๊ณ„ ๋ฐ ๋ถ„ํฌ ๋ถ„์„ + +#### 5.1 ์ดˆ๊ธฐ ๋ชฉํ‘œ ๋ฐ์ดํ„ฐ์…‹ ๊ทœ๋ชจ + +| ์นดํ…Œ๊ณ ๋ฆฌ | ๋ชฉํ‘œ ๊ฑด์ˆ˜ | ์ˆ˜์ง‘ ๋ฐฉ๋ฒ• | +|---------|----------|----------| +| ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ (๊ณต๊ณต/SNS) | 300๊ฑด | ํฌ๋กค๋ง + API | +| ๋ฒค์น˜๋งˆํฌ ์‚ฌ๋ก€ | 100๊ฑด | ์ˆ˜๋™ ์ˆ˜์ง‘ | +| ์ž์‚ฌ ๋ฐ์ดํ„ฐ (์ดˆ๊ธฐ) | 0๊ฑด | - | +| **์ด๊ณ„** | **400๊ฑด** | - | + +#### 5.2 ๋ฐ์ดํ„ฐ ๋ถ„ํฌ ๋ชฉํ‘œ + +**์—…์ข…๋ณ„ ๋ถ„ํฌ** +- ์Œ์‹์ : 40% +- ์†Œ๋งค์ : 25% +- ์„œ๋น„์Šค: 20% +- ์ˆ™๋ฐ•: 10% +- ๊ธฐํƒ€: 5% + +**์˜ˆ์‚ฐ๋ณ„ ๋ถ„ํฌ** +- ์ €๋น„์šฉ (50๋งŒ์› ์ดํ•˜): 40% +- ์ค‘๋น„์šฉ (50~200๋งŒ์›): 35% +- ๊ณ ๋น„์šฉ (200๋งŒ์› ์ด์ƒ): 25% + +**์ง€์—ญ๋ณ„ ๋ถ„ํฌ** +- ์„œ์šธ: 30% +- ๊ฒฝ๊ธฐ: 25% +- ๋ถ€์‚ฐ/๋Œ€๊ตฌ/์ธ์ฒœ: 20% +- ๊ธฐํƒ€ ์ง€์—ญ: 25% + +--- + +## Claude API ์—ฐ๋™ ๊ตฌ์กฐ + +### 1. API ํ˜ธ์ถœ ์ „๋žต + +#### 1.1 ๋‹จ์ผ ํ˜ธ์ถœ + Structured Output ๋ฐฉ์‹ (์ตœ์ข… ์„ ํƒ) + +**์„ ํƒ ์ด์œ ** +- ์‘๋‹ต ์‹œ๊ฐ„ ๋‹จ์ถ• (10์ดˆ ๋‚ด ๋ณด์žฅ) +- API ๋น„์šฉ ์ ˆ๊ฐ +- ํŠธ๋ Œ๋“œ์™€ ์ถ”์ฒœ์˜ ์ผ๊ด€์„ฑ ์œ ์ง€ +- Structured Output์œผ๋กœ JSON ํŒŒ์‹ฑ ์•ˆ์ •์„ฑ ํ–ฅ์ƒ + +**ํ˜ธ์ถœ ํ”Œ๋กœ์šฐ** +``` +1. ์‚ฌ์šฉ์ž ์š”์ฒญ โ†’ AI ์„œ๋น„์Šค +2. ์œ ์‚ฌ ์ด๋ฒคํŠธ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ (Pinecone) +3. ์บ์‹œ ํ™•์ธ (Redis) +4. Claude API ๋‹จ์ผ ํ˜ธ์ถœ (ํŠธ๋ Œ๋“œ + 6๊ฐœ ์ถ”์ฒœ) +5. ์‘๋‹ต ํŒŒ์‹ฑ ๋ฐ ๊ฒ€์ฆ +6. ์บ์‹œ ์ €์žฅ +7. ํ”„๋ก ํŠธ์—”๋“œ ์‘๋‹ต +``` + +### 2. JSON ์š”์ฒญ/์‘๋‹ต ๊ตฌ์กฐ + +#### 2.1 Claude API ์š”์ฒญ ๊ตฌ์กฐ + +```json +{ + "model": "claude-3-5-sonnet-20241022", + "max_tokens": 4096, + "temperature": 0.7, + "system": "๋‹น์‹ ์€ ์†Œ์ƒ๊ณต์ธ์„ ์œ„ํ•œ ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. ์ฃผ์–ด์ง„ ๋งค์žฅ ์ •๋ณด์™€ ์ด๋ฒคํŠธ ๋ชฉ์ ์„ ๋ฐ”ํƒ•์œผ๋กœ ํšจ๊ณผ์ ์ธ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ์„ ์ œ์•ˆํ•ฉ๋‹ˆ๋‹ค.", + "messages": [ + { + "role": "user", + "content": "๋‹ค์Œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ์ถ”์ฒœํ•ด์ฃผ์„ธ์š”:\n\n[๋งค์žฅ ์ •๋ณด]\n- ์—…์ข…: ์Œ์‹์  (๊ณ ๊นƒ์ง‘)\n- ์ง€์—ญ: ์„œ์šธ ๊ฐ•๋‚จ๊ตฌ\n- ์ด๋ฒคํŠธ ๋ชฉ์ : ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜\n- ํ˜„์žฌ ์‹œ์ฆŒ: 2025๋…„ 1์›” (๊ฒจ์šธ)\n\n[์ฐธ๊ณ  ๋ฐ์ดํ„ฐ]\n๊ณผ๊ฑฐ ์œ ์‚ฌํ•œ ๋งค์žฅ์—์„œ ์„ฑ๊ณตํ•œ ์ด๋ฒคํŠธ:\n1. SNS ํŒ”๋กœ์šฐ ์ด๋ฒคํŠธ - ์ฐธ์—ฌ์ž 200๋ช…, ๋น„์šฉ 30๋งŒ์›, ROI 450%\n2. ๋ฆฌ๋ทฐ ์ž‘์„ฑ ์ด๋ฒคํŠธ - ์ฐธ์—ฌ์ž 180๋ช…, ๋น„์šฉ 120๋งŒ์›, ROI 380%\n...\n\n๋‹ค์Œ ํ˜•์‹์œผ๋กœ ์‘๋‹ตํ•ด์ฃผ์„ธ์š”:\n1. ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ ๋ถ„์„\n2. 3๊ฐ€์ง€ ์˜ˆ์‚ฐ๋ณ„ ์ด๋ฒคํŠธ ์ถ”์ฒœ (๊ฐ ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ 1๊ฐœ์”ฉ)" + } + ], + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "event_recommendations", + "strict": true, + "schema": { + "type": "object", + "properties": { + "trends": { + "type": "object", + "properties": { + "industry": {"type": "string"}, + "location": {"type": "string"}, + "season": {"type": "string"} + }, + "required": ["industry", "location", "season"] + }, + "recommendations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "budget": {"type": "string", "enum": ["low", "medium", "high"]}, + "type": {"type": "string", "enum": ["online", "offline"]}, + "title": {"type": "string"}, + "prize": {"type": "string"}, + "participation": {"type": "string"}, + "expectedParticipants": {"type": "integer"}, + "cost": {"type": "integer"}, + "roi": {"type": "integer"} + }, + "required": ["budget", "type", "title", "prize", "participation", "expectedParticipants", "cost", "roi"] + }, + "minItems": 6, + "maxItems": 6 + } + }, + "required": ["trends", "recommendations"] + } + } + } +} +``` + +#### 2.2 Claude API ์‘๋‹ต ๊ตฌ์กฐ + +```json +{ + "id": "msg_01...", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "{\"trends\":{\"industry\":\"์Œ์‹์ ์—… ์‹ ๋…„ ํ”„๋กœ๋ชจ์…˜ ํŠธ๋ Œ๋“œ: 1์›”์€ ์ƒˆํ•ด ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜๋ฅผ ์œ„ํ•œ ํ• ์ธ ์ด๋ฒคํŠธ์™€ SNS ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ…์ด ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค. ํŠนํžˆ ๊ณ ๊นƒ์ง‘์€ ๋‹จ์ฒด ํ• ์ธ ๋ฐ ์žฌ๋ฐฉ๋ฌธ ์ฟ ํฐ ์ œ๊ณต์ด ์ธ๊ธฐ์ž…๋‹ˆ๋‹ค.\",\"location\":\"๊ฐ•๋‚จ๊ตฌ๋Š” ์ง์žฅ์ธ ๋ฐ MZ์„ธ๋Œ€ ๊ณ ๊ฐ์ด ๋งŽ์•„ SNS ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ์™€ ์ ์‹ฌ ํŠน๊ฐ€ ์ด๋ฒคํŠธ๊ฐ€ ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค. ํŠนํžˆ Instagram๊ณผ ๋„ค์ด๋ฒ„ ๋ธ”๋กœ๊ทธ๋ฅผ ํ™œ์šฉํ•œ ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ…์ด ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.\",\"season\":\"๊ฒจ์šธ ์‹œ์ฆŒ์—๋Š” ๋”ฐ๋œปํ•œ ์‹ค๋‚ด ์ด๋ฒคํŠธ์™€ ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ ํ”„๋กœ๋ชจ์…˜์ด ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค. ์„ค ์—ฐํœด ๋Œ€๋น„ ๊ฐ€์กฑ ๋‹จ์œ„ ๊ณ ๊ฐ ํƒ€๊ฒŸํŒ…์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.\"},\"recommendations\":[{\"budget\":\"low\",\"type\":\"online\",\"title\":\"SNS ํŒ”๋กœ์šฐ ์ด๋ฒคํŠธ\",\"prize\":\"์ปคํ”ผ ์ฟ ํฐ\",\"participation\":\"Instagram ํŒ”๋กœ์šฐ + ๊ฒŒ์‹œ๋ฌผ ๊ณต์œ \",\"expectedParticipants\":180,\"cost\":250000,\"roi\":520},{\"budget\":\"low\",\"type\":\"offline\",\"title\":\"์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก ์ด๋ฒคํŠธ\",\"prize\":\"์ปคํ”ผ ์ฟ ํฐ\",\"participation\":\"๋งค์žฅ ๋ฐฉ๋ฌธ ์‹œ ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก\",\"expectedParticipants\":150,\"cost\":300000,\"roi\":450},{\"budget\":\"medium\",\"type\":\"online\",\"title\":\"๋ฆฌ๋ทฐ ์ž‘์„ฑ ์ด๋ฒคํŠธ\",\"prize\":\"5์ฒœ์› ์ƒํ’ˆ๊ถŒ\",\"participation\":\"๋„ค์ด๋ฒ„ ๋ธ”๋กœ๊ทธ ๋ฆฌ๋ทฐ ์ž‘์„ฑ\",\"expectedParticipants\":250,\"cost\":1500000,\"roi\":380},{\"budget\":\"medium\",\"type\":\"offline\",\"title\":\"๋ฐฉ๋ฌธ ๋„์žฅ ์ ๋ฆฝ ์ด๋ฒคํŠธ\",\"prize\":\"๋ฌด๋ฃŒ ์‹์‚ฌ๊ถŒ\",\"participation\":\"5ํšŒ ๋ฐฉ๋ฌธ ์‹œ ๋„์žฅ ์ ๋ฆฝ\",\"expectedParticipants\":200,\"cost\":1800000,\"roi\":320},{\"budget\":\"high\",\"type\":\"online\",\"title\":\"์ธํ”Œ๋ฃจ์–ธ์„œ ํ˜‘์—… ์ด๋ฒคํŠธ\",\"prize\":\"1๋งŒ์› ํ• ์ธ๊ถŒ\",\"participation\":\"์ธํ”Œ๋ฃจ์–ธ์„œ ๊ฒŒ์‹œ๋ฌผ ์ข‹์•„์š” + ํŒ”๋กœ์šฐ\",\"expectedParticipants\":400,\"cost\":5000000,\"roi\":280},{\"budget\":\"high\",\"type\":\"offline\",\"title\":\"VIP ๊ณ ๊ฐ ์ดˆ๋Œ€ ์ด๋ฒคํŠธ\",\"prize\":\"ํŠน๋ณ„ ๋ฉ”๋‰ด ์ œ๊ณต\",\"participation\":\"VIP ์ดˆ๋Œ€์žฅ ๋ฐœ์†ก\",\"expectedParticipants\":300,\"cost\":6000000,\"roi\":240}]}" + } + ], + "model": "claude-3-5-sonnet-20241022", + "usage": { + "input_tokens": 1205, + "output_tokens": 856 + } +} +``` + +#### 2.3 ์„œ๋น„์Šค ๋ ˆ์ด์–ด ์‘๋‹ต ๊ตฌ์กฐ (ํ”„๋ก ํŠธ์—”๋“œ๋กœ ์ „๋‹ฌ) + +```json +{ + "success": true, + "data": { + "trends": { + "industry": "์Œ์‹์ ์—… ์‹ ๋…„ ํ”„๋กœ๋ชจ์…˜ ํŠธ๋ Œ๋“œ: 1์›”์€ ์ƒˆํ•ด ์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜๋ฅผ ์œ„ํ•œ ํ• ์ธ ์ด๋ฒคํŠธ์™€ SNS ๋ฐ”์ด๋Ÿด ๋งˆ์ผ€ํŒ…์ด ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค...", + "location": "๊ฐ•๋‚จ๊ตฌ๋Š” ์ง์žฅ์ธ ๋ฐ MZ์„ธ๋Œ€ ๊ณ ๊ฐ์ด ๋งŽ์•„ SNS ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ์™€ ์ ์‹ฌ ํŠน๊ฐ€ ์ด๋ฒคํŠธ๊ฐ€ ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค...", + "season": "๊ฒจ์šธ ์‹œ์ฆŒ์—๋Š” ๋”ฐ๋œปํ•œ ์‹ค๋‚ด ์ด๋ฒคํŠธ์™€ ์žฌ๋ฐฉ๋ฌธ ์œ ๋„ ํ”„๋กœ๋ชจ์…˜์ด ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค..." + }, + "recommendations": [ + { + "id": "low-online", + "budget": "low", + "type": "online", + "title": "SNS ํŒ”๋กœ์šฐ ์ด๋ฒคํŠธ", + "prize": "์ปคํ”ผ ์ฟ ํฐ", + "participation": "Instagram ํŒ”๋กœ์šฐ + ๊ฒŒ์‹œ๋ฌผ ๊ณต์œ ", + "expectedParticipants": 180, + "cost": 250000, + "roi": 520 + }, + { + "id": "low-offline", + "budget": "low", + "type": "offline", + "title": "์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก ์ด๋ฒคํŠธ", + "prize": "์ปคํ”ผ ์ฟ ํฐ", + "participation": "๋งค์žฅ ๋ฐฉ๋ฌธ ์‹œ ์ „ํ™”๋ฒˆํ˜ธ ๋“ฑ๋ก", + "expectedParticipants": 150, + "cost": 300000, + "roi": 450 + }, + { + "id": "medium-online", + "budget": "medium", + "type": "online", + "title": "๋ฆฌ๋ทฐ ์ž‘์„ฑ ์ด๋ฒคํŠธ", + "prize": "5์ฒœ์› ์ƒํ’ˆ๊ถŒ", + "participation": "๋„ค์ด๋ฒ„ ๋ธ”๋กœ๊ทธ ๋ฆฌ๋ทฐ ์ž‘์„ฑ", + "expectedParticipants": 250, + "cost": 1500000, + "roi": 380 + }, + { + "id": "medium-offline", + "budget": "medium", + "type": "offline", + "title": "๋ฐฉ๋ฌธ ๋„์žฅ ์ ๋ฆฝ ์ด๋ฒคํŠธ", + "prize": "๋ฌด๋ฃŒ ์‹์‚ฌ๊ถŒ", + "participation": "5ํšŒ ๋ฐฉ๋ฌธ ์‹œ ๋„์žฅ ์ ๋ฆฝ", + "expectedParticipants": 200, + "cost": 1800000, + "roi": 320 + }, + { + "id": "high-online", + "budget": "high", + "type": "online", + "title": "์ธํ”Œ๋ฃจ์–ธ์„œ ํ˜‘์—… ์ด๋ฒคํŠธ", + "prize": "1๋งŒ์› ํ• ์ธ๊ถŒ", + "participation": "์ธํ”Œ๋ฃจ์–ธ์„œ ๊ฒŒ์‹œ๋ฌผ ์ข‹์•„์š” + ํŒ”๋กœ์šฐ", + "expectedParticipants": 400, + "cost": 5000000, + "roi": 280 + }, + { + "id": "high-offline", + "budget": "high", + "type": "offline", + "title": "VIP ๊ณ ๊ฐ ์ดˆ๋Œ€ ์ด๋ฒคํŠธ", + "prize": "ํŠน๋ณ„ ๋ฉ”๋‰ด ์ œ๊ณต", + "participation": "VIP ์ดˆ๋Œ€์žฅ ๋ฐœ์†ก", + "expectedParticipants": 300, + "cost": 6000000, + "roi": 240 + } + ] + }, + "metadata": { + "cacheHit": false, + "processingTime": 8.5, + "modelUsage": { + "inputTokens": 1205, + "outputTokens": 856 + } + } +} +``` + +### 3. ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง + +#### 3.1 System Prompt + +``` +๋‹น์‹ ์€ ์†Œ์ƒ๊ณต์ธ์„ ์œ„ํ•œ ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. + +[์—ญํ• ] +- ๋งค์žฅ ์ •๋ณด์™€ ์ด๋ฒคํŠธ ๋ชฉ์ ์„ ๋ฐ”ํƒ•์œผ๋กœ ํšจ๊ณผ์ ์ธ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ ์ œ์•ˆ +- ์—…์ข…๋ณ„, ์ง€์—ญ๋ณ„, ์‹œ์ฆŒ๋ณ„ ํŠธ๋ Œ๋“œ ๋ถ„์„ +- ์˜ˆ์‚ฐ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์ด๋ฒคํŠธ ์ถ”์ฒœ + +[์ œ์•ฝ์‚ฌํ•ญ] +- ์ถ”์ฒœ์€ ๋ฐ˜๋“œ์‹œ 6๊ฐœ (์ €/์ค‘/๊ณ  ์˜ˆ์‚ฐ ร— ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ) +- ๋ชจ๋“  ์ถ”์ฒœ์€ ์‹คํ˜„ ๊ฐ€๋Šฅํ•˜๊ณ  ๊ตฌ์ฒด์ ์ด์–ด์•ผ ํ•จ +- ์˜ˆ์ƒ ์ฐธ์—ฌ์ž, ๋น„์šฉ, ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ ์€ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ํ˜„์‹ค์  ์ˆ˜์น˜ +- ๊ฒฝํ’ˆ์€ ์˜ˆ์‚ฐ ๋ฒ”์œ„ ๋‚ด์—์„œ ์‹คํ˜„ ๊ฐ€๋Šฅํ•œ ๊ฒƒ + +[์‘๋‹ต ํ˜•์‹] +- JSON ํ˜•์‹์œผ๋กœ ์‘๋‹ต +- trends: ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ ๋ถ„์„ (๊ฐ 100์ž ๋‚ด์™ธ) +- recommendations: 6๊ฐœ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ ๋ฐฐ์—ด +``` + +#### 3.2 User Prompt ํ…œํ”Œ๋ฆฟ + +```python +def build_user_prompt(store_info, event_purpose, similar_events): + return f""" +๋‹ค์Œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ์ถ”์ฒœํ•ด์ฃผ์„ธ์š”: + +[๋งค์žฅ ์ •๋ณด] +- ์—…์ข…: {store_info['industry']} ({store_info['businessType']}) +- ์ง€์—ญ: {store_info['location']['city']} {store_info['location']['district']} +- ์ด๋ฒคํŠธ ๋ชฉ์ : {event_purpose} +- ํ˜„์žฌ ์‹œ์ฆŒ: {get_current_season()} + +[์ฐธ๊ณ  ๋ฐ์ดํ„ฐ] +๊ณผ๊ฑฐ ์œ ์‚ฌํ•œ ๋งค์žฅ์—์„œ ์„ฑ๊ณตํ•œ ์ด๋ฒคํŠธ: +{format_similar_events(similar_events)} + +[์š”๊ตฌ์‚ฌํ•ญ] +1. ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ ๋ถ„์„ (๊ฐ 100์ž ๋‚ด์™ธ) +2. 3๊ฐ€์ง€ ์˜ˆ์‚ฐ๋ณ„ ์ด๋ฒคํŠธ ์ถ”์ฒœ: + - ์ €๋น„์šฉ (25~30๋งŒ์›): ์˜จ๋ผ์ธ 1๊ฐœ, ์˜คํ”„๋ผ์ธ 1๊ฐœ + - ์ค‘๋น„์šฉ (150~180๋งŒ์›): ์˜จ๋ผ์ธ 1๊ฐœ, ์˜คํ”„๋ผ์ธ 1๊ฐœ + - ๊ณ ๋น„์šฉ (500~600๋งŒ์›): ์˜จ๋ผ์ธ 1๊ฐœ, ์˜คํ”„๋ผ์ธ 1๊ฐœ + +๊ฐ ์ถ”์ฒœ์—๋Š” ๋‹ค์Œ ์ •๋ณด๋ฅผ ํฌํ•จ: +- ์ด๋ฒคํŠธ ์ œ๋ชฉ +- ๊ฒฝํ’ˆ๋ช… +- ์ฐธ์—ฌ ๋ฐฉ๋ฒ• +- ์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜ +- ์˜ˆ์ƒ ๋น„์šฉ (์› ๋‹จ์œ„) +- ์˜ˆ์ƒ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  (%) +""" + +def format_similar_events(events): + formatted = [] + for i, event in enumerate(events, 1): + formatted.append(f"{i}. {event['title']} - ์ฐธ์—ฌ์ž {event['participants']}๋ช…, ๋น„์šฉ {event['cost']:,}์›, ROI {event['roi']}%") + return "\n".join(formatted) +``` + +#### 3.3 Few-shot ์˜ˆ์ œ (ํ•„์š” ์‹œ) + +```python +FEW_SHOT_EXAMPLES = [ + { + "input": { + "industry": "์Œ์‹์ ", + "location": "์„œ์šธ ๊ฐ•๋‚จ๊ตฌ", + "purpose": "์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜", + "season": "๊ฒจ์šธ" + }, + "output": { + "trends": { + "industry": "์Œ์‹์ ์—… ์‹ ๋…„ ํ”„๋กœ๋ชจ์…˜ ํŠธ๋ Œ๋“œ...", + "location": "๊ฐ•๋‚จ๊ตฌ๋Š” ์ง์žฅ์ธ ๋ฐ MZ์„ธ๋Œ€ ๊ณ ๊ฐ์ด ๋งŽ์•„...", + "season": "๊ฒจ์šธ ์‹œ์ฆŒ์—๋Š” ๋”ฐ๋œปํ•œ ์‹ค๋‚ด ์ด๋ฒคํŠธ..." + }, + "recommendations": [...] + } + } +] +``` + +### 4. ๋ฐฑ์—”๋“œ ๊ตฌํ˜„ (Node.js) + +#### 4.1 AI ์„œ๋น„์Šค ์ปจํŠธ๋กค๋Ÿฌ + +```javascript +// controllers/aiController.js +const aiService = require('../services/aiService'); +const cacheService = require('../services/cacheService'); + +exports.getEventRecommendations = async (req, res) => { + try { + const { eventPurpose, storeInfo } = req.body; + const userId = req.user.id; + + // 1. ์บ์‹œ ํ‚ค ์ƒ์„ฑ + const cacheKey = `recommendations:${storeInfo.industry}:${storeInfo.location.district}:${eventPurpose}`; + + // 2. ์บ์‹œ ํ™•์ธ + const cached = await cacheService.get(cacheKey); + if (cached) { + return res.json({ + success: true, + data: cached, + metadata: { cacheHit: true } + }); + } + + // 3. AI ์ถ”์ฒœ ์ƒ์„ฑ + const startTime = Date.now(); + const recommendations = await aiService.generateRecommendations({ + eventPurpose, + storeInfo, + season: getCurrentSeason() + }); + const processingTime = (Date.now() - startTime) / 1000; + + // 4. ์บ์‹œ ์ €์žฅ (15๋ถ„ TTL) + await cacheService.set(cacheKey, recommendations, 900); + + // 5. ์‘๋‹ต + res.json({ + success: true, + data: recommendations, + metadata: { + cacheHit: false, + processingTime, + modelUsage: recommendations.usage + } + }); + + } catch (error) { + console.error('AI recommendation error:', error); + res.status(500).json({ + success: false, + error: 'AI ์ถ”์ฒœ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.' + }); + } +}; + +function getCurrentSeason() { + const month = new Date().getMonth() + 1; + if ([12, 1, 2].includes(month)) return '๊ฒจ์šธ'; + if ([3, 4, 5].includes(month)) return '๋ด„'; + if ([6, 7, 8].includes(month)) return '์—ฌ๋ฆ„'; + return '๊ฐ€์„'; +} +``` + +#### 4.2 AI ์„œ๋น„์Šค ๋ ˆ์ด์–ด + +```javascript +// services/aiService.js +const Anthropic = require('@anthropic-ai/sdk'); +const pineconeService = require('./pineconeService'); + +const anthropic = new Anthropic({ + apiKey: process.env.CLAUDE_API_KEY +}); + +exports.generateRecommendations = async ({ eventPurpose, storeInfo, season }) => { + try { + // 1. ์œ ์‚ฌ ์ด๋ฒคํŠธ ๊ฒ€์ƒ‰ + const similarEvents = await pineconeService.searchSimilarEvents({ + industry: storeInfo.industry, + location: storeInfo.location.district, + season + }); + + // 2. ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ + const userPrompt = buildUserPrompt(storeInfo, eventPurpose, season, similarEvents); + + // 3. Claude API ํ˜ธ์ถœ + const message = await anthropic.messages.create({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 4096, + temperature: 0.7, + system: getSystemPrompt(), + messages: [{ role: 'user', content: userPrompt }], + response_format: { + type: 'json_schema', + json_schema: getResponseSchema() + } + }); + + // 4. ์‘๋‹ต ํŒŒ์‹ฑ + const content = message.content[0].text; + const result = JSON.parse(content); + + // 5. ID ์ถ”๊ฐ€ ๋ฐ ๊ฒ€์ฆ + result.recommendations = result.recommendations.map((rec, idx) => ({ + id: `${rec.budget}-${rec.type}`, + ...rec + })); + + // 6. ๊ฒ€์ฆ + validateRecommendations(result); + + return { + ...result, + usage: message.usage + }; + + } catch (error) { + console.error('Claude API error:', error); + + // Fallback: ๊ธฐ๋ณธ ์ถ”์ฒœ ๋ฐ˜ํ™˜ + return getFallbackRecommendations(storeInfo); + } +}; + +function buildUserPrompt(storeInfo, eventPurpose, season, similarEvents) { + const purposeMap = { + '์‹ ๊ทœ๊ณ ๊ฐ์œ ์น˜': '์‹ ๊ทœ ๊ณ ๊ฐ ์œ ์น˜', + '์žฌ๋ฐฉ๋ฌธ์œ ๋„': '์žฌ๋ฐฉ๋ฌธ ์œ ๋„', + '๋งค์ถœ์ฆ๋Œ€': '๋งค์ถœ ์ฆ๋Œ€', + '์ธ์ง€๋„ํ–ฅ์ƒ': '์ธ์ง€๋„ ํ–ฅ์ƒ' + }; + + return ` +๋‹ค์Œ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ์ถ”์ฒœํ•ด์ฃผ์„ธ์š”: + +[๋งค์žฅ ์ •๋ณด] +- ์—…์ข…: ${storeInfo.industry} (${storeInfo.businessType}) +- ์ง€์—ญ: ${storeInfo.location.city} ${storeInfo.location.district} +- ์ด๋ฒคํŠธ ๋ชฉ์ : ${purposeMap[eventPurpose]} +- ํ˜„์žฌ ์‹œ์ฆŒ: ${season} + +[์ฐธ๊ณ  ๋ฐ์ดํ„ฐ] +๊ณผ๊ฑฐ ์œ ์‚ฌํ•œ ๋งค์žฅ์—์„œ ์„ฑ๊ณตํ•œ ์ด๋ฒคํŠธ: +${formatSimilarEvents(similarEvents)} + +[์š”๊ตฌ์‚ฌํ•ญ] +1. ์—…์ข…/์ง€์—ญ/์‹œ์ฆŒ ํŠธ๋ Œ๋“œ ๋ถ„์„ (๊ฐ 100์ž ๋‚ด์™ธ) +2. 3๊ฐ€์ง€ ์˜ˆ์‚ฐ๋ณ„ ์ด๋ฒคํŠธ ์ถ”์ฒœ: + - ์ €๋น„์šฉ (25~30๋งŒ์›): ์˜จ๋ผ์ธ 1๊ฐœ, ์˜คํ”„๋ผ์ธ 1๊ฐœ + - ์ค‘๋น„์šฉ (150~180๋งŒ์›): ์˜จ๋ผ์ธ 1๊ฐœ, ์˜คํ”„๋ผ์ธ 1๊ฐœ + - ๊ณ ๋น„์šฉ (500~600๋งŒ์›): ์˜จ๋ผ์ธ 1๊ฐœ, ์˜คํ”„๋ผ์ธ 1๊ฐœ + +๊ฐ ์ถ”์ฒœ์—๋Š” ๋‹ค์Œ ์ •๋ณด๋ฅผ ํฌํ•จ: +- ์ด๋ฒคํŠธ ์ œ๋ชฉ (30์ž ์ด๋‚ด) +- ๊ฒฝํ’ˆ๋ช… (20์ž ์ด๋‚ด) +- ์ฐธ์—ฌ ๋ฐฉ๋ฒ• (๊ฐ„๊ฒฐํ•˜๊ฒŒ) +- ์˜ˆ์ƒ ์ฐธ์—ฌ์ž ์ˆ˜ (์ •์ˆ˜) +- ์˜ˆ์ƒ ๋น„์šฉ (์› ๋‹จ์œ„, ์ •์ˆ˜) +- ์˜ˆ์ƒ ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ  (%, ์ •์ˆ˜) +`.trim(); +} + +function formatSimilarEvents(events) { + return events.map((e, i) => + `${i + 1}. ${e.metadata.title} - ์ฐธ์—ฌ์ž ${e.metadata.participants}๋ช…, ๋น„์šฉ ${e.metadata.cost.toLocaleString()}์›, ROI ${e.metadata.roi}%` + ).join('\n'); +} + +function getSystemPrompt() { + return `๋‹น์‹ ์€ ์†Œ์ƒ๊ณต์ธ์„ ์œ„ํ•œ ์ด๋ฒคํŠธ ๋งˆ์ผ€ํŒ… ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. + +[์—ญํ• ] +- ๋งค์žฅ ์ •๋ณด์™€ ์ด๋ฒคํŠธ ๋ชฉ์ ์„ ๋ฐ”ํƒ•์œผ๋กœ ํšจ๊ณผ์ ์ธ ์ด๋ฒคํŠธ ๊ธฐํš์•ˆ ์ œ์•ˆ +- ์—…์ข…๋ณ„, ์ง€์—ญ๋ณ„, ์‹œ์ฆŒ๋ณ„ ํŠธ๋ Œ๋“œ ๋ถ„์„ +- ์˜ˆ์‚ฐ๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์ด๋ฒคํŠธ ์ถ”์ฒœ + +[์ œ์•ฝ์‚ฌํ•ญ] +- ์ถ”์ฒœ์€ ๋ฐ˜๋“œ์‹œ 6๊ฐœ (์ €/์ค‘/๊ณ  ์˜ˆ์‚ฐ ร— ์˜จ๋ผ์ธ/์˜คํ”„๋ผ์ธ) +- ๋ชจ๋“  ์ถ”์ฒœ์€ ์‹คํ˜„ ๊ฐ€๋Šฅํ•˜๊ณ  ๊ตฌ์ฒด์ ์ด์–ด์•ผ ํ•จ +- ์˜ˆ์ƒ ์ฐธ์—ฌ์ž, ๋น„์šฉ, ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ ์€ ๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ํ˜„์‹ค์  ์ˆ˜์น˜ +- ๊ฒฝํ’ˆ์€ ์˜ˆ์‚ฐ ๋ฒ”์œ„ ๋‚ด์—์„œ ์‹คํ˜„ ๊ฐ€๋Šฅํ•œ ๊ฒƒ +- ํˆฌ์ž๋Œ€๋น„์ˆ˜์ต๋ฅ ์€ ์ €์˜ˆ์‚ฐ์ผ์ˆ˜๋ก ๋†’๊ณ , ๊ณ ์˜ˆ์‚ฐ์ผ์ˆ˜๋ก ๋‚ฎ๊ฒŒ ์„ค์ •`; +} + +function getResponseSchema() { + return { + name: 'event_recommendations', + strict: true, + schema: { + type: 'object', + properties: { + trends: { + type: 'object', + properties: { + industry: { type: 'string' }, + location: { type: 'string' }, + season: { type: 'string' } + }, + required: ['industry', 'location', 'season'] + }, + recommendations: { + type: 'array', + items: { + type: 'object', + properties: { + budget: { type: 'string', enum: ['low', 'medium', 'high'] }, + type: { type: 'string', enum: ['online', 'offline'] }, + title: { type: 'string' }, + prize: { type: 'string' }, + participation: { type: 'string' }, + expectedParticipants: { type: 'integer' }, + cost: { type: 'integer' }, + roi: { type: 'integer' } + }, + required: ['budget', 'type', 'title', 'prize', 'participation', 'expectedParticipants', 'cost', 'roi'] + }, + minItems: 6, + maxItems: 6 + } + }, + required: ['trends', 'recommendations'] + } + }; +} + +function validateRecommendations(result) { + // 6๊ฐœ ์ถ”์ฒœ ๊ฒ€์ฆ + if (result.recommendations.length !== 6) { + throw new Error('์ถ”์ฒœ ๊ฐœ์ˆ˜๊ฐ€ 6๊ฐœ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค'); + } + + // ์˜ˆ์‚ฐ๋ณ„, ํƒ€์ž…๋ณ„ ๊ฐœ์ˆ˜ ๊ฒ€์ฆ + const counts = {}; + result.recommendations.forEach(rec => { + const key = `${rec.budget}-${rec.type}`; + counts[key] = (counts[key] || 0) + 1; + }); + + const expected = { + 'low-online': 1, + 'low-offline': 1, + 'medium-online': 1, + 'medium-offline': 1, + 'high-online': 1, + 'high-offline': 1 + }; + + for (const [key, count] of Object.entries(expected)) { + if (counts[key] !== count) { + throw new Error(`${key} ์ถ”์ฒœ ๊ฐœ์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค`); + } + } +} + +function getFallbackRecommendations(storeInfo) { + // API ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ์ถ”์ฒœ ๋ฐ˜ํ™˜ + return { + trends: { + industry: `${storeInfo.industry} ์—…์ข…์˜ ์ผ๋ฐ˜์ ์ธ ํŠธ๋ Œ๋“œ์ž…๋‹ˆ๋‹ค.`, + location: `${storeInfo.location.district} ์ง€์—ญ์˜ ์ผ๋ฐ˜์ ์ธ ํŠน์„ฑ์ž…๋‹ˆ๋‹ค.`, + season: '๊ณ„์ ˆ๋ณ„ ์ผ๋ฐ˜์ ์ธ ์ด๋ฒคํŠธ ํŠน์„ฑ์ž…๋‹ˆ๋‹ค.' + }, + recommendations: [ + { + id: 'low-online', + budget: 'low', + type: 'online', + title: 'SNS ํŒ”๋กœ์šฐ ์ด๋ฒคํŠธ', + prize: '์ปคํ”ผ ์ฟ ํฐ', + participation: 'SNS ํŒ”๋กœ์šฐ', + expectedParticipants: 150, + cost: 250000, + roi: 500 + }, + // ... ๋‚˜๋จธ์ง€ 5๊ฐœ + ] + }; +} +``` + +#### 4.3 Pinecone ์„œ๋น„์Šค + +```javascript +// services/pineconeService.js +const { PineconeClient } = require('@pinecone-database/pinecone'); +const openai = require('openai'); + +const pinecone = new PineconeClient(); +await pinecone.init({ + apiKey: process.env.PINECONE_API_KEY, + environment: process.env.PINECONE_ENV +}); + +const index = pinecone.Index('kt-event-recommendations'); + +exports.searchSimilarEvents = async ({ industry, location, season }) => { + try { + // 1. ์ฟผ๋ฆฌ ํ…์ŠคํŠธ ์ƒ์„ฑ + const queryText = `์—…์ข…: ${industry}, ์ง€์—ญ: ${location}, ์‹œ์ฆŒ: ${season}`; + + // 2. ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ + const embedding = await generateEmbedding(queryText); + + // 3. ํ•„ํ„ฐ ์กฐ๊ฑด + const filter = { + industry: { $eq: industry } + }; + + // 4. ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ + const results = await index.query({ + vector: embedding, + filter, + topK: 5, + includeMetadata: true + }); + + return results.matches; + + } catch (error) { + console.error('Pinecone search error:', error); + return []; + } +}; + +async function generateEmbedding(text) { + const response = await openai.embeddings.create({ + model: 'text-embedding-3-large', + input: text + }); + return response.data[0].embedding; +} +``` + +### 5. ํƒ€์ž„์•„์›ƒ ๋ฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ + +```javascript +// utils/timeout.js +exports.withTimeout = (promise, timeoutMs) => { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), timeoutMs) + ) + ]); +}; + +// ์‚ฌ์šฉ ์˜ˆ์‹œ +const recommendations = await withTimeout( + aiService.generateRecommendations(params), + 10000 // 10์ดˆ +); +``` + +--- + +## ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ + +### 1. ์ „์ฒด ์•„ํ‚คํ…์ฒ˜ ๋‹ค์ด์–ด๊ทธ๋žจ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Frontend โ”‚ +โ”‚ (React) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ HTTP/REST + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ API Gateway (Express) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ–ผ โ–ผ โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚User Service โ”‚ โ”‚Event Service โ”‚ โ”‚AI Service โ”‚ โ”‚Analytics โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ–ผ โ–ผ โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚Redis Cache โ”‚ โ”‚Pinecone โ”‚ โ”‚Claude APIโ”‚ + โ”‚- Trends (1h) โ”‚ โ”‚Vector DB โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚- Similar (30m) โ”‚ โ”‚- Event Embeddingsโ”‚ + โ”‚- Results (15m) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚PostgreSQL โ”‚ + โ”‚- Users โ”‚ + โ”‚- Events โ”‚ + โ”‚- Performance โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2. AI ์„œ๋น„์Šค ์ƒ์„ธ ํ”Œ๋กœ์šฐ + +``` +์‚ฌ์šฉ์ž ์š”์ฒญ + โ”‚ + โ–ผ +[1] ์š”์ฒญ ์ˆ˜์‹  + โ”‚ + โ–ผ +[2] ์บ์‹œ ํ™•์ธ (Redis) + โ”‚ + โ”œโ”€ Hit โ†’ ์ฆ‰์‹œ ์‘๋‹ต (< 100ms) + โ”‚ + โ””โ”€ Miss + โ”‚ + โ–ผ + [3] ์œ ์‚ฌ ์ด๋ฒคํŠธ ๊ฒ€์ƒ‰ (Pinecone) + โ”‚ - ์ฟผ๋ฆฌ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ (OpenAI API) + โ”‚ - ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ (์ฝ”์‚ฌ์ธ ์œ ์‚ฌ๋„) + โ”‚ - Top 5 ๋ฐ˜ํ™˜ + โ–ผ + [4] ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ + โ”‚ - ๋งค์žฅ ์ •๋ณด + ์ด๋ฒคํŠธ ๋ชฉ์  + โ”‚ - ์œ ์‚ฌ ์ด๋ฒคํŠธ ์ปจํ…์ŠคํŠธ + โ”‚ - Few-shot ์˜ˆ์ œ + โ–ผ + [5] Claude API ํ˜ธ์ถœ + โ”‚ - Structured Output + โ”‚ - ํƒ€์ž„์•„์›ƒ: 8์ดˆ + โ–ผ + [6] ์‘๋‹ต ํŒŒ์‹ฑ ๋ฐ ๊ฒ€์ฆ + โ”‚ - JSON ํŒŒ์‹ฑ + โ”‚ - 6๊ฐœ ์ถ”์ฒœ ๊ฒ€์ฆ + โ”‚ - ์˜ˆ์‚ฐ/ํƒ€์ž… ๊ฒ€์ฆ + โ–ผ + [7] ์บ์‹œ ์ €์žฅ (Redis) + โ”‚ - TTL: 15๋ถ„ + โ–ผ + [8] ํ”„๋ก ํŠธ์—”๋“œ ์‘๋‹ต + โ”‚ - ์ด ์†Œ์š”์‹œ๊ฐ„: < 10์ดˆ +``` + +### 3. ๋ฐ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ + +``` +[์ผ์ผ ๋ฐฐ์น˜ ์ž‘์—…] (Airflow) + โ”‚ + โ”œโ”€ [์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘] + โ”‚ โ”œโ”€ ๊ณต๊ณต๋ฐ์ดํ„ฐ API + โ”‚ โ”œโ”€ SNS ํฌ๋กค๋ง + โ”‚ โ””โ”€ ๋ฒค์น˜๋งˆํฌ ์‚ฌ๋ก€ + โ”‚ + โ–ผ +[๋ฐ์ดํ„ฐ ์ •์ œ] + โ”‚ - ์ค‘๋ณต ์ œ๊ฑฐ + โ”‚ - ์ด์ƒ์น˜ ํƒ์ง€ + โ”‚ - ํƒœ๊น… + โ–ผ +[PostgreSQL ์ €์žฅ] + โ”‚ + โ–ผ +[๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ] + โ”‚ - OpenAI API + โ”‚ - ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ + โ–ผ +[Pinecone ์ €์žฅ] + + +[์‹ค์‹œ๊ฐ„ ์ด๋ฒคํŠธ ์ƒ์„ฑ] + โ”‚ + โ–ผ +[PostgreSQL ์ €์žฅ] + โ”‚ + โ–ผ +[๋น„๋™๊ธฐ ํ (RabbitMQ)] + โ”‚ + โ–ผ +[๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ] + โ”‚ + โ–ผ +[Pinecone ์—…๋ฐ์ดํŠธ] +``` + +--- + +## ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต + +### 1. Redis ์บ์‹ฑ ์ „๋žต + +#### 1.1 3๋‹จ๊ณ„ ์บ์‹ฑ + +```javascript +// services/cacheService.js +const redis = require('redis'); +const client = redis.createClient({ + host: process.env.REDIS_HOST, + port: process.env.REDIS_PORT, + password: process.env.REDIS_PASSWORD +}); + +// ๋ ˆ๋ฒจ 1: ํŠธ๋ Œ๋“œ ๋ถ„์„ ์บ์‹ฑ (1์‹œ๊ฐ„ TTL) +exports.getTrendCache = async (industry, location, season) => { + const key = `trend:${industry}:${location}:${season}`; + const cached = await client.get(key); + return cached ? JSON.parse(cached) : null; +}; + +exports.setTrendCache = async (industry, location, season, data) => { + const key = `trend:${industry}:${location}:${season}`; + await client.setex(key, 3600, JSON.stringify(data)); +}; + +// ๋ ˆ๋ฒจ 2: ์œ ์‚ฌ ์ด๋ฒคํŠธ ๊ฒ€์ƒ‰ ์บ์‹ฑ (30๋ถ„ TTL) +exports.getSimilarEventsCache = async (industry, location, season) => { + const key = `similar:${industry}:${location}:${season}`; + const cached = await client.get(key); + return cached ? JSON.parse(cached) : null; +}; + +exports.setSimilarEventsCache = async (industry, location, season, data) => { + const key = `similar:${industry}:${location}:${season}`; + await client.setex(key, 1800, JSON.stringify(data)); +}; + +// ๋ ˆ๋ฒจ 3: ์ „์ฒด ์ถ”์ฒœ ๊ฒฐ๊ณผ ์บ์‹ฑ (15๋ถ„ TTL) +exports.getRecommendationCache = async (industry, location, purpose) => { + const key = `recommendations:${industry}:${location}:${purpose}`; + const cached = await client.get(key); + return cached ? JSON.parse(cached) : null; +}; + +exports.setRecommendationCache = async (industry, location, purpose, data) => { + const key = `recommendations:${industry}:${location}:${purpose}`; + await client.setex(key, 900, JSON.stringify(data)); +}; +``` + +#### 1.2 ์บ์‹œ ๋ฌดํšจํ™” ์ „๋žต + +```javascript +// ์ƒˆ๋กœ์šด ์ด๋ฒคํŠธ ์„ฑ๊ณผ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹œ ๊ด€๋ จ ์บ์‹œ ๋ฌดํšจํ™” +exports.invalidateRelatedCache = async (eventData) => { + const patterns = [ + `trend:${eventData.industry}:*`, + `similar:${eventData.industry}:*`, + `recommendations:${eventData.industry}:*` + ]; + + for (const pattern of patterns) { + const keys = await client.keys(pattern); + if (keys.length > 0) { + await client.del(...keys); + } + } +}; +``` + +### 2. ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ์ตœ์ ํ™” + +```javascript +// services/aiService.js +exports.generateRecommendations = async ({ eventPurpose, storeInfo, season }) => { + // ๋ณ‘๋ ฌ ์‹คํ–‰ + const [similarEvents, trendCache] = await Promise.all([ + // ์œ ์‚ฌ ์ด๋ฒคํŠธ ๊ฒ€์ƒ‰ + pineconeService.searchSimilarEvents({ + industry: storeInfo.industry, + location: storeInfo.location.district, + season + }), + + // ํŠธ๋ Œ๋“œ ์บ์‹œ ํ™•์ธ + cacheService.getTrendCache( + storeInfo.industry, + storeInfo.location.district, + season + ) + ]); + + // ... ๋‚˜๋จธ์ง€ ๋กœ์ง +}; +``` + +### 3. Rate Limiting ๋Œ€์‘ + +```javascript +// utils/rateLimiter.js +const rateLimit = require('express-rate-limit'); + +// Claude API Rate Limit ๋Œ€์‘ +const apiLimiter = rateLimit({ + windowMs: 60 * 1000, // 1๋ถ„ + max: 50, // ๋ถ„๋‹น ์ตœ๋Œ€ 50๊ฑด (Claude API ์ œํ•œ์— ๋งž์ถค) + message: '์š”์ฒญ์ด ๋„ˆ๋ฌด ๋งŽ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.', + handler: async (req, res) => { + // Rate limit ์ดˆ๊ณผ ์‹œ ์บ์‹œ๋œ ๊ธฐ๋ณธ ์ถ”์ฒœ ๋ฐ˜ํ™˜ + const fallback = await getFallbackRecommendations(req.body.storeInfo); + res.status(200).json({ + success: true, + data: fallback, + metadata: { + rateLimited: true, + message: '์ผ์‹œ์ ์œผ๋กœ ๊ธฐ๋ณธ ์ถ”์ฒœ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค' + } + }); + } +}); + +module.exports = apiLimiter; +``` + +### 4. ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ์ตœ์ ํ™” + +```python +# Pinecone ์ธ๋ฑ์Šค ํŒŒํ‹ฐ์…”๋‹ +# ์—…์ข…๋ณ„๋กœ ๋„ค์ž„์ŠคํŽ˜์ด์Šค ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฒ€์ƒ‰ ์„ฑ๋Šฅ ํ–ฅ์ƒ + +def save_to_pinecone_with_namespace(event): + industry_namespace = event['industry'] + + pinecone_index.upsert( + vectors=[{ + 'id': event['id'], + 'values': embedding, + 'metadata': {...} + }], + namespace=industry_namespace # ์—…์ข…๋ณ„ ๋„ค์ž„์ŠคํŽ˜์ด์Šค + ) + +def search_with_namespace(query_event): + industry_namespace = query_event['industry'] + + results = pinecone_index.query( + vector=query_embedding, + namespace=industry_namespace, # ๋™์ผ ์—…์ข… ๋‚ด์—์„œ๋งŒ ๊ฒ€์ƒ‰ + top_k=5 + ) + return results +``` + +### 5. ์‘๋‹ต ์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง + +```javascript +// middleware/performanceMonitor.js +exports.trackPerformance = async (req, res, next) => { + const start = Date.now(); + + res.on('finish', () => { + const duration = Date.now() - start; + + // 10์ดˆ ์ดˆ๊ณผ ์‹œ ๊ฒฝ๊ณ  + if (duration > 10000) { + console.warn(`Slow request: ${req.path} took ${duration}ms`); + + // ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ์— ์ „์†ก (์˜ˆ: Datadog, New Relic) + sendMetric('ai.recommendation.slow', { + path: req.path, + duration, + industry: req.body.storeInfo?.industry + }); + } + + // ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ ์ถ”์  + sendMetric('ai.recommendation.duration', duration); + }); + + next(); +}; +``` + +--- + +## ๊ตฌํ˜„ ๋กœ๋“œ๋งต + +### Phase 1: ๊ธฐ๋ณธ ์ธํ”„๋ผ ๊ตฌ์ถ• (2์ฃผ) + +**Week 1** +- [ ] Pinecone ๊ณ„์ • ์ƒ์„ฑ ๋ฐ ์ธ๋ฑ์Šค ์„ค์ • +- [ ] Redis ํด๋Ÿฌ์Šคํ„ฐ ๊ตฌ์ถ• +- [ ] PostgreSQL ์Šคํ‚ค๋งˆ ์„ค๊ณ„ +- [ ] Claude API ํ‚ค ๋ฐœ๊ธ‰ ๋ฐ ํ…Œ์ŠคํŠธ + +**Week 2** +- [ ] ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์Šคํฌ๋ฆฝํŠธ ๊ฐœ๋ฐœ +- [ ] ETL ํŒŒ์ดํ”„๋ผ์ธ (Airflow DAG) ๊ตฌ์ถ• +- [ ] ๋ฐ์ดํ„ฐ ์ •์ œ ๋กœ์ง ๊ตฌํ˜„ +- [ ] ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ์…‹ ๊ตฌ์ถ• (300๊ฑด) + +### Phase 2: AI ์„œ๋น„์Šค ๊ฐœ๋ฐœ (3์ฃผ) + +**Week 3** +- [ ] Claude API ์—ฐ๋™ ๊ธฐ๋ณธ ๊ตฌ์กฐ +- [ ] ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ ๊ฐœ๋ฐœ +- [ ] Structured Output ์Šคํ‚ค๋งˆ ์„ค๊ณ„ +- [ ] ์‘๋‹ต ํŒŒ์‹ฑ ๋ฐ ๊ฒ€์ฆ ๋กœ์ง + +**Week 4** +- [ ] ๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ๋กœ์ง +- [ ] Pinecone ์—ฐ๋™ (์ €์žฅ/๊ฒ€์ƒ‰) +- [ ] ์œ ์‚ฌ ์ด๋ฒคํŠธ ๊ฒ€์ƒ‰ ์•Œ๊ณ ๋ฆฌ์ฆ˜ +- [ ] ์บ์‹ฑ ๋ ˆ์ด์–ด ๊ตฌํ˜„ + +**Week 5** +- [ ] AI ์„œ๋น„์Šค ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ +- [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” (๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ) +- [ ] ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ Fallback ๊ตฌํ˜„ +- [ ] ์‘๋‹ต ์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง + +### Phase 3: ํ”„๋ก ํŠธ์—”๋“œ ์—ฐ๋™ (1์ฃผ) + +**Week 6** +- [ ] API ์—”๋“œํฌ์ธํŠธ ์—ฐ๋™ +- [ ] ๋กœ๋”ฉ ์ƒํƒœ UI ๊ตฌํ˜„ +- [ ] ์—๋Ÿฌ ํ•ธ๋“ค๋ง UI +- [ ] E2E ํ…Œ์ŠคํŠธ + +### Phase 4: ์šด์˜ ๋ฐ ๊ฐœ์„  (์ง€์†์ ) + +**Week 7+** +- [ ] ์‹ค์‚ฌ์šฉ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹œ์ž‘ +- [ ] ์ถ”์ฒœ ์ •ํ™•๋„ ๋ชจ๋‹ˆํ„ฐ๋ง +- [ ] ํ”„๋กฌํ”„ํŠธ ์ตœ์ ํ™” +- [ ] A/B ํ…Œ์ŠคํŠธ ์ง„ํ–‰ +- [ ] ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ๋ฐ˜์˜ + +--- + +## ๋น„์šฉ ์˜ˆ์ธก + +### 1. Claude API ๋น„์šฉ + +**๊ฐ€์ •** +- ์›” ์‚ฌ์šฉ์ž ์ˆ˜: 1,000๋ช… +- ์‚ฌ์šฉ์ž๋‹น ์›” ํ‰๊ท  ์ด๋ฒคํŠธ ์ƒ์„ฑ: 3ํšŒ +- ์›” ์ด API ํ˜ธ์ถœ: 3,000ํšŒ + +**ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰ (์˜ˆ์ƒ)** +- Input: 1,200 tokens/ํ˜ธ์ถœ +- Output: 800 tokens/ํ˜ธ์ถœ + +**๋น„์šฉ ๊ณ„์‚ฐ** (Claude 3.5 Sonnet ๊ธฐ์ค€) +- Input: $3 / 1M tokens = $0.003 / 1K tokens +- Output: $15 / 1M tokens = $0.015 / 1K tokens + +``` +์›” ๋น„์šฉ = (1,200 * 0.003 + 800 * 0.015) * 3,000 + = (3.6 + 12) * 3,000 + = 15.6 * 3,000 + = $46,800 / ์›” +``` + +### 2. OpenAI Embeddings API ๋น„์šฉ + +**๊ฐ€์ •** +- ์ผ์ผ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘: 10๊ฑด +- ์›” ์ˆ˜์ง‘: 300๊ฑด +- ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ ์ƒ์„ฑ: 3,000๊ฑด/์›” + +**๋น„์šฉ ๊ณ„์‚ฐ** (text-embedding-3-large ๊ธฐ์ค€) +- $0.13 / 1M tokens + +``` +์›” ๋น„์šฉ = (300 + 3,000) * 100 tokens * 0.13 / 1,000,000 + = 3,300 * 100 * 0.00000013 + = $0.04 / ์›” +``` + +### 3. Pinecone ๋น„์šฉ + +**๊ฐ€์ •** +- ๋ฒกํ„ฐ ์ฐจ์›: 3,072 +- ์ €์žฅ ๋ฒกํ„ฐ ์ˆ˜: 10,000๊ฑด +- ์›” ์ฟผ๋ฆฌ ์ˆ˜: 3,000ํšŒ + +**๋น„์šฉ ๊ณ„์‚ฐ** (Starter Plan) +- $70 / ์›” (100,000 ๋ฒกํ„ฐ ํฌํ•จ) + +### 4. Redis ๋น„์šฉ + +**๊ฐ€์ •** +- AWS ElastiCache (cache.t3.micro) + +**๋น„์šฉ ๊ณ„์‚ฐ** +- $0.017 / ์‹œ๊ฐ„ = $12.24 / ์›” + +### 5. ์ด ๋น„์šฉ + +| ํ•ญ๋ชฉ | ์›” ๋น„์šฉ | +|------|--------| +| Claude API | $46,800 | +| OpenAI Embeddings | $0.04 | +| Pinecone | $70 | +| Redis | $12.24 | +| **์ด๊ณ„** | **$46,882** | + +**๋น„์šฉ ์ ˆ๊ฐ ๋ฐฉ์•ˆ** +1. ์บ์‹ฑ ํ™œ์šฉ๋ฅ  ํ–ฅ์ƒ (๋ชฉํ‘œ: 50% ์บ์‹œ ํžˆํŠธ์œจ) + - Claude API ๋น„์šฉ 50% ์ ˆ๊ฐ โ†’ $23,400 +2. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ตœ์ ํ™” +3. ์‚ฌ์šฉ๋Ÿ‰ ๊ธฐ๋ฐ˜ ๋™์  ์Šค์ผ€์ผ๋ง + +--- + +## ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ฐœ์„  + +### 1. ํ•ต์‹ฌ ์ง€ํ‘œ + +**์„ฑ๋Šฅ ์ง€ํ‘œ** +- ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„: < 10์ดˆ +- ์บ์‹œ ํžˆํŠธ์œจ: > 50% +- API ์„ฑ๊ณต๋ฅ : > 99% + +**ํ’ˆ์งˆ ์ง€ํ‘œ** +- ์ถ”์ฒœ ์„ ํƒ๋ฅ : > 70% (์‚ฌ์šฉ์ž๊ฐ€ 6๊ฐœ ์ค‘ 1๊ฐœ ์ด์ƒ ์„ ํƒ) +- "๋‹ค์‹œ ์ถ”์ฒœ๋ฐ›๊ธฐ" ๋น„์œจ: < 30% +- ์˜ˆ์ธก ์ •ํ™•๋„: ์‹ค์ œ ROI์™€ ์˜ˆ์ธก ROI ์ฐจ์ด < 20% + +### 2. ๊ฐœ์„  ์‚ฌ์ดํด + +``` +[๋ฐ์ดํ„ฐ ์ˆ˜์ง‘] + โ”‚ + โ–ผ +[์ •ํ™•๋„ ๋ถ„์„] + โ”‚ - ์˜ˆ์ธก vs ์‹ค์ œ ๋น„๊ต + โ”‚ - ์—…์ข…๋ณ„/์ง€์—ญ๋ณ„ ๋ถ„์„ + โ–ผ +[ํ”„๋กฌํ”„ํŠธ ์ตœ์ ํ™”] + โ”‚ - Few-shot ์˜ˆ์ œ ๊ฐœ์„  + โ”‚ - ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์กฐ์ • + โ–ผ +[A/B ํ…Œ์ŠคํŠธ] + โ”‚ - ์ƒˆ ๋ฒ„์ „ vs ๊ธฐ์กด ๋ฒ„์ „ + โ”‚ - ์Šน์ž ์„ ํƒ + โ–ผ +[๋ฐฐํฌ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง] +``` + +--- + +## ๋ถ€๋ก + +### A. ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ + +#### PostgreSQL ํ…Œ์ด๋ธ” ๊ตฌ์กฐ + +```sql +-- ์ด๋ฒคํŠธ ํ…Œ์ด๋ธ” +CREATE TABLE events ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + title VARCHAR(100), + prize VARCHAR(50), + participation VARCHAR(200), + industry VARCHAR(50), + location_city VARCHAR(50), + location_district VARCHAR(50), + season VARCHAR(10), + cost INTEGER, + expected_roi INTEGER, + created_at TIMESTAMP DEFAULT NOW() +); + +-- ์ด๋ฒคํŠธ ์„ฑ๊ณผ ํ…Œ์ด๋ธ” +CREATE TABLE event_performance ( + id SERIAL PRIMARY KEY, + event_id INTEGER REFERENCES events(id), + actual_participants INTEGER, + actual_cost INTEGER, + actual_roi INTEGER, + completed_at TIMESTAMP DEFAULT NOW() +); + +-- AI ์ถ”์ฒœ ์ด๋ ฅ ํ…Œ์ด๋ธ” +CREATE TABLE ai_recommendations ( + id SERIAL PRIMARY KEY, + event_id INTEGER REFERENCES events(id), + recommendation_data JSONB, -- Claude API ์‘๋‹ต ์ „์ฒด + selected_option VARCHAR(20), -- ์˜ˆ: "low-online" + created_at TIMESTAMP DEFAULT NOW() +); + +-- ์ธ๋ฑ์Šค +CREATE INDEX idx_events_industry ON events(industry); +CREATE INDEX idx_events_location ON events(location_district); +CREATE INDEX idx_events_season ON events(season); +CREATE INDEX idx_performance_event ON event_performance(event_id); +``` + +### B. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • + +```bash +# .env +# Claude API +CLAUDE_API_KEY=sk-ant-xxx + +# OpenAI +OPENAI_API_KEY=sk-xxx + +# Pinecone +PINECONE_API_KEY=xxx +PINECONE_ENV=us-west1-gcp +PINECONE_INDEX=kt-event-recommendations + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD=xxx + +# PostgreSQL +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=kt_events +DB_USER=postgres +DB_PASSWORD=xxx +``` + +### C. ์ฐธ๊ณ  ์ž๋ฃŒ + +**Claude API** +- [Anthropic Documentation](https://docs.anthropic.com/) +- [Structured Outputs Guide](https://docs.anthropic.com/en/docs/build-with-claude/structured-outputs) + +**Pinecone** +- [Pinecone Documentation](https://docs.pinecone.io/) +- [Vector Search Best Practices](https://www.pinecone.io/learn/vector-search/) + +**OpenAI Embeddings** +- [OpenAI Embeddings Guide](https://platform.openai.com/docs/guides/embeddings) + +--- + +**๋ฌธ์„œ ๋**