diff --git a/.github/workflows/deploy-getcloser.yml b/.github/workflows/deploy-getcloser.yml index 58ad93c..14b9af3 100644 --- a/.github/workflows/deploy-getcloser.yml +++ b/.github/workflows/deploy-getcloser.yml @@ -39,10 +39,14 @@ jobs: set -e echo "DB_USER=${{ secrets.DB_USER }}" > .env echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> .env + echo "SWAGGER_USER=${{ secrets.SWAGGER_USER }}" >> .env + echo "SWAGGER_PASSWORD=${{ secrets.SWAGGER_PASSWORD }}" >> .env echo "DB_DATABASE=${{ vars.DB_DATABASE }}" >> .env + echo "DB_PORT=${{ vars.DB_PORT }}" >> .env echo "APP_HOST=${{ vars.APP_HOST }}" >> .env echo "TEAM_SIZE=${{ vars.TEAM_SIZE}}" >> .env echo "PENDING_TIMEOUT_MINUTES=${{ vars.PENDING_TIMEOUT_MINUTES}}" >> .env + echo "DATA_DIR_HOST=${{ vars.DATA_DIR_HOST }}" >> .env - name: ๐Ÿš€ Deploy to PROD run: | diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8613b6d..0267636 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,6 +6,7 @@ on: - main paths: - 'book/**' + workflow_dispatch: jobs: build-and-deploy: diff --git a/.github/workflows/devfactory-homepage.yml b/.github/workflows/devfactory-homepage.yml new file mode 100644 index 0000000..b7b99a9 --- /dev/null +++ b/.github/workflows/devfactory-homepage.yml @@ -0,0 +1,43 @@ +name: ๐Ÿš€ DevFactory Homepage Deploy +run-name: ๐Ÿš€ Deploying to Production by @${{ github.actor }} + +on: + push: + branches: + - main + paths: + - 'platform/**' + - '.github/workflows/devfactory-homepage.yml' + workflow_dispatch: + +# ๊ฐ™์€ ๋ธŒ๋žœ์น˜ ๋™์‹œ ์‹คํ–‰ ์‹œ ์ด์ „ ์žก ์ทจ์†Œ(๊ฒฝ์Ÿ ๋ฐฐํฌ ๋ฐฉ์ง€) +concurrency: + group: DevFactory-homepage-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy-prod: + if: github.ref_name == 'main' + name: ๐Ÿš€ Deploy DF-platform (Production) + runs-on: oracle + environment: platform + defaults: + run: + working-directory: ./platform + steps: + - uses: actions/checkout@v4 + + - name: Write .env (prod) + run: | + cat > .env <<'EOF' + APP_HOST=${{ vars.APP_HOST }} + DATABASE_URL=${{ secrets.DATABASE_URL }} + EOF + + - name: Build & up (prod) + run: | + set -euxo pipefail + docker compose -p df-platform-main config -q + docker compose -p df-platform-main down --remove-orphans + docker compose -p df-platform-main up -d --build --remove-orphans + docker image prune -f --filter "label=org.pseudolab.project=devfactory-platform" diff --git a/README.en.md b/README.en.md index 53a4aea..590ebb0 100644 --- a/README.en.md +++ b/README.en.md @@ -6,6 +6,7 @@
PseudoLab +PseudoLab Discord Community Stars Badge Forks Badge @@ -17,9 +18,9 @@ -> Welcome to the DevFactory repository! -> We are building Pseudo-Labโ€™s developer culture through various tutorials and AI service development projects. -> With empathy, communication, and collaboration at our core, we aim to create a culture where everyone can grow together. +> Welcome to the DevFactory repository! +> We are building Pseudo-Labโ€™s developer culture through various tutorials and AI service development projects. +> With empathy, communication, and collaboration at our core, we aim to create a culture where everyone can grow together. > Ideas and feedback about our projects are always welcome โ€” feel free to reach out anytime!
@@ -29,93 +30,117 @@ - **E-mail**: soohyun.dev@gmail.com โ€” Builder: Soohyun Kim
-## ๐ŸŒŸ Projects -At DevFactory, we are shaping Pseudo-Labโ€™s unique developer ecosystem through the following activities ๐Ÿค— +## ๐ŸŒŸ Projects +Highlight activities of DevFactory ๐Ÿค— - - - - - - +* **๐Ÿณ Technical Tutorials (Tutorials)** + * Hands-on content and offline workshops covering Docker, Git, LLM, etc. + * [๐Ÿ”— View Tutorials](https://pseudo-lab.github.io/DevFactory/intro.html) + +* **๐ŸŽฎ Networking Event (BINGO)** + * A web application where keyword-based bingo games facilitate natural networking. + * [๐Ÿ”— Visit Site](https://bingo.pseudolab-devfactory.com/) + +* **๐ŸŽฎ Networking Event (Get Closer)** + * A web application to get to know each other through simple quizzes. + * [๐Ÿ”— Visit Site](https://getcloser.pseudolab-devfactory.com/) + +* **๐Ÿ“œ Certificate Issuance System** + * A service that tracks Pseudo-Lab's growth, issuing and managing certificates for activities. + * [๐Ÿ”— Visit Site](https://cert.pseudo-lab.com/) + +* **๐Ÿค– JobPT** + * AI-powered personalized career support solution. A tool that helps developer career growth through resume analysis and interview feedback. + * [๐Ÿ”— Visit Site](http://jobpt.pseudolab-devfactory.com/) + +
+ +### ๐Ÿ”Ž Activities by Batch + +- [DevFactory 10th Gen Activity Page](docs/10th_plan.md) +- [DevFactory 11th Gen Activity Page](docs/11th_plan.md) + +## ๐Ÿง‘ Meet the Team + +
๐Ÿณ Tutorial๐ŸŽฎ Networking Event (BINGO)๐Ÿ“ฆ PseudoLab TOOLBOX
- - - - - - - - -
- We design and run a variety of technical tutorials.
- From Docker and Git to LLMs, we offer hands-on content and offline workshops where everyone can learn and grow together. -
- An open-source networking bingo web application, freely available for anyone to use.
- It encourages casual conversations through light, keyword-based bingo games. -
- A support platform for participating in and managing Pseudo-Lab activities.
- This web service handles study applications, certificate issuance, and more.
- (In development) +
+ Soohyun Kim
+
+ + +
+ +
+ +
- -
- View tutorial -
+
+ Yesin Kim
+
+ +
+ +
+ +
- -
- View on GitHub -
+
+ Seungkyu Kim
+
+ +
+ +
+ +
- -
- Visit site -
+
+ Yunhee Hwang
+
+ +
+
+ +
- -## ๐Ÿง‘ ํŒ€์› ์†Œ๊ฐœ - - - - - +
-

- Soohyun Kim
- PM / Infra
- Major Experience: HDC Labs, AI Engineer
- - - - -
- ๐Ÿ”— Github | - Blog | - LinkedIn | - Book +
+ Yujin Choi
+
+ +
+
+ +
-

- Yesin Kim
- Backend / DB
- Major Experience: AI Talk, NLP Engineer
- - -
- ๐Ÿ”— Github | - Blog | - LinkedIn +
+ Jong-il Seok
+
+ +
+
+ +
-

- Seungkyu Kim
- Frontend
- Major Experience: Imagoworks , Data Engineer
- - -
- ๐Ÿ”— Github | - LinkedIn +
+ Hyeonjun Jung
+
+ +
+
+ + +
+ Nayeon Han
+
+ +
+
+ +
diff --git a/README.md b/README.md index a61d6b3..62a4632 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@
PseudoLab +PseudoLab Discord Community Stars Badge Forks Badge @@ -29,98 +30,112 @@ - **E-mail**: soohyun.dev@gmail.com โ€” Builder: ๊น€์ˆ˜ํ˜„ -## ๐ŸŒŸ ํ”„๋กœ์ ํŠธ -DevFactory์—์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ํ™œ๋™์„ ํ†ตํ•ด ๊ฐ€์งœ์—ฐ๊ตฌ์†Œ๋งŒ์˜ ๊ฐœ๋ฐœ ์ƒํƒœ๊ณ„๋ฅผ ๋งŒ๋“ค์–ด๊ฐ€๋Š” ์ค‘์ž…๋‹ˆ๋‹ค ๐Ÿค— +## ๐ŸŒŸ ํ”„๋กœ์ ํŠธ +DevFactory์˜ ์ฃผ์š” ํ™œ๋™ ๋‚ด์—ญ์ž…๋‹ˆ๋‹ค ๐Ÿค— +* **๐Ÿณ ๊ธฐ์ˆ  ํŠœํ† ๋ฆฌ์–ผ (Tutorials)** + * Docker, Git, LLM ๋“ฑ ์‹ค์Šต ์ค‘์‹ฌ์˜ ์˜จ๋ผ์ธ ์ฝ˜ํ…์ธ ์™€ ์˜คํ”„๋ผ์ธ ์›Œํฌ์ˆ์„ ์šด์˜ํ•ฉ๋‹ˆ๋‹ค. + * [๐Ÿ”— ํŠœํ† ๋ฆฌ์–ผ ๋ณด๊ธฐ](https://pseudo-lab.github.io/DevFactory/intro.html) - - - - - - - - - - - - - - - - -
๐Ÿณ Tutorial๐ŸŽฎ ๋„คํŠธ์›Œํ‚น ์ด๋ฒคํŠธ (BINGO)๐Ÿ“ฆ PseudoLab TOOLBOX
- ๋‹ค์–‘ํ•œ ๊ธฐ์ˆ  ํŠœํ† ๋ฆฌ์–ผ์„ ๊ธฐํšยท์šด์˜ํ•ฉ๋‹ˆ๋‹ค.
- Docker, Git, LLM ๋“ฑ ์‹ค์Šต ์ค‘์‹ฌ์˜ ์ฝ˜ํ…์ธ ์™€ ์˜คํ”„๋ผ์ธ ์›Œํฌ์ˆ์„ ํ†ตํ•ด ํ•จ๊ป˜ ๋ฐฐ์šฐ๊ณ  ์„ฑ์žฅํ•ฉ๋‹ˆ๋‹ค. -
- ๋ˆ„๊ตฌ๋‚˜ ์ž์œ ๋กญ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์˜คํ”ˆ์†Œ์Šค๋กœ ๊ณต๊ฐœํ•œ ๋„คํŠธ์›Œํ‚น ๋น™๊ณ  ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค.
- ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜์˜ ๋น™๊ณ  ๊ฒŒ์ž„์„ ํ†ตํ•ด ๊ฐ€๋ฒผ์šด ๋Œ€ํ™”๋ฅผ ์œ ๋„ํ•˜๊ณ  ๋„คํŠธ์›Œํ‚น์„ ๋•์Šต๋‹ˆ๋‹ค. -
- ๊ฐ€์งœ์—ฐ๊ตฌ์†Œ ์ฐธ์—ฌยท์šด์˜์„ ์œ„ํ•œ ์ง€์› ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค.
- ์Šคํ„ฐ๋”” ์‹ ์ฒญ, ์ˆ˜๋ฃŒ์ฆ ๋ฐœ๊ธ‰ ๋“ฑ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์›น ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค.
- (๊ฐœ๋ฐœ ์ค‘) -
- -
- ํŠœํ† ๋ฆฌ์–ผ ๋ณด๊ธฐ -
-
- -
- GitHub ์ €์žฅ์†Œ ์—ด๊ธฐ -
-
- -
- ์›น์‚ฌ์ดํŠธ ์—ด๊ธฐ -
-
+* **๐ŸŽฎ ๋„คํŠธ์›Œํ‚น ํ”„๋กœ๊ทธ๋žจ (BINGO)** + * ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜์˜ ๋น™๊ณ  ๊ฒŒ์ž„์„ ํ†ตํ•ด ์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™”๋ฅผ ์œ ๋„ํ•˜๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค. + * [๐Ÿ”— ์›น์‚ฌ์ดํŠธ ํ™•์ธ](https://bingo.pseudolab-devfactory.com/) +* **๐ŸŽฎ ๋„คํŠธ์›Œํ‚น ํ”„๋กœ๊ทธ๋žจ (์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ)** + * ๊ฐ„๋‹จํ•œ ํ€ด์ฆˆ๋ฅผ ํ†ตํ•ด ์„œ๋กœ๋ฅผ ์•Œ์•„๊ฐ€๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค. + * [๐Ÿ”— ์›น์‚ฌ์ดํŠธ ํ™•์ธ](https://getcloser.pseudolab-devfactory.com/) -### ๐Ÿ”Ž ๊ธฐ์ˆ˜๋ณ„ ํ™œ๋™ +* **๐Ÿ“œ ์ˆ˜๋ฃŒ์ฆ ๋ฐœ๊ธ‰ ์‹œ์Šคํ…œ** + * ๊ฐ€์งœ์—ฐ๊ตฌ์†Œ์˜ ์„ฑ์žฅ์„ ๊ธฐ๋กํ•˜๋Š” ์„œ๋น„์Šค๋กœ, ํ™œ๋™ ์ˆ˜๋ฃŒ์ฆ์„ ๋ฐœ๊ธ‰ํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * [๐Ÿ”— ์›น์‚ฌ์ดํŠธ ํ™•์ธ](https://cert.pseudo-lab.com/) +* **๐Ÿค– JobPT** + * AI ๊ธฐ๋ฐ˜ ๊ฐœ์ธํ™” ์ทจ์—… ์ง€์› ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. ์ด๋ ฅ์„œ ๋ถ„์„๋ถ€ํ„ฐ ๋ฉด์ ‘ ํ”ผ๋“œ๋ฐฑ๊นŒ์ง€ ๊ฐœ๋ฐœ์ž์˜ ์ปค๋ฆฌ์–ด ์„ฑ์žฅ์„ ๋•๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. + * [๐Ÿ”— ์„œ๋น„์Šค ๋ฐ”๋กœ๊ฐ€๊ธฐ](http://jobpt.pseudolab-devfactory.com/) -- [DevFactory 10๊ธฐ ํ™œ๋™ ํŽ˜์ด์ง€](docs/10th_plan.md) +### ๐Ÿ”Ž ๊ธฐ์ˆ˜๋ณ„ ํ™œ๋™ +* [DevFactory 10๊ธฐ ํ™œ๋™ ํŽ˜์ด์ง€](docs/10th_plan.md) +* [DevFactory 11๊ธฐ ํ™œ๋™ ํŽ˜์ด์ง€](docs/11th_plan.md) ## ๐Ÿง‘ ํŒ€์› ์†Œ๊ฐœ - +
- - - + + + + + + +
-

+
๊น€์ˆ˜ํ˜„
- PM / Infra
- ์ฃผ์š”๊ฒฝ๋ ฅ: HDC ๋žฉ์Šค, AI Engineer
- - - - -
- ๐Ÿ”— Github | - Blog | - LinkedIn | - Book +
+ + +
+ +
+ +
-

+
๊น€์˜ˆ์‹ 
- Backend / DB
- ์ฃผ์š”๊ฒฝ๋ ฅ: ์—์ด์•„์ดํ†ก, NLP Engineer
- - -
- ๐Ÿ”— Github | - Blog | - LinkedIn +
+ +
+ +
+ +
-

+
๊น€์Šน๊ทœ
- Frontend
- ์ฃผ์š”๊ฒฝ๋ ฅ: ์ด๋งˆ๊ณ ์›์Šค, Data Engineer
- - -
- ๐Ÿ”— Github | - LinkedIn +
+ +
+ +
+ + +
+ ํ™ฉ์œคํฌ
+
+ +
+
+ + +
+ ์ตœ์œ ์ง„
+
+ +
+
+ + +
+ ์„์ข…์ผ
+
+ +
+
+ + +
+ ์ •ํ˜„์ค€
+
+ +
+
+ + +
+ ํ•œ๋‚˜์—ฐ
+
+ +
+
+ +
diff --git a/cert/backend/config/default_periods.json b/cert/backend/config/default_periods.json index e011b2a..7608448 100644 --- a/cert/backend/config/default_periods.json +++ b/cert/backend/config/default_periods.json @@ -1,4 +1,6 @@ { + "1": { "start": "2020-10-11", "end": "2020-12-19" }, + "2": { "start": "2021-01-10", "end": "2021-03-27" }, "3": { "start": "2021-07-16", "end": "2021-10-08" }, "4": { "start": "2022-03-07", "end": "2022-05-29" }, "5": { "start": "2022-09-05", "end": "2022-11-27" }, diff --git a/cert/backend/src/utils/access_log.py b/cert/backend/src/utils/access_log.py index 1162d17..d5d440d 100644 --- a/cert/backend/src/utils/access_log.py +++ b/cert/backend/src/utils/access_log.py @@ -93,6 +93,26 @@ async def close_access_log(app: FastAPI) -> None: await pool.close() +def _get_client_ip(request: Request) -> Optional[str]: + """Extracts the client IP address from request headers or client info.""" + # Check X-Forwarded-For header (common for reverse proxies) + forwarded_for = request.headers.get("X-Forwarded-For") + if forwarded_for: + # X-Forwarded-For can be a comma-separated list; the first one is the original client + return forwarded_for.split(",")[0].strip() + + # Check X-Real-IP header + real_ip = request.headers.get("X-Real-IP") + if real_ip: + return real_ip + + # Fallback to request.client.host + if request.client: + return request.client.host + + return None + + async def log_request( request: Request, response: Optional[Response], @@ -108,7 +128,8 @@ async def log_request( if request.url.path in config.exclude_paths: return - ip_hash = _hash_ip(getattr(request.client, "host", None), config.ip_salt) + client_ip = _get_client_ip(request) + ip_hash = _hash_ip(client_ip, config.ip_salt) user_agent = request.headers.get("user-agent") referrer = request.headers.get("referer") or request.headers.get("referrer") @@ -141,7 +162,8 @@ async def log_page_view(request: Request, page_path: str) -> None: if not config or not pool: return - ip_hash = _hash_ip(getattr(request.client, "host", None), config.ip_salt) + client_ip = _get_client_ip(request) + ip_hash = _hash_ip(client_ip, config.ip_salt) user_agent = request.headers.get("user-agent") referrer = request.headers.get("referer") or request.headers.get("referrer") diff --git a/docs/11th_plan.md b/docs/11th_plan.md new file mode 100644 index 0000000..25adc01 --- /dev/null +++ b/docs/11th_plan.md @@ -0,0 +1,57 @@ +# DevFactory 11th + +## ๐ŸŒŸ ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ (Project Vision) +_"๊ฐ€์งœ์—ฐ๊ตฌ์†Œ์˜ ๊ฐœ๋ฐœ ๋ฌธํ™”๋ฅผ ๋งŒ๋“ค์–ด๊ฐ€๋Š” DevFactory"_ +- 2nd Grand Gathering์—์„œ ์šด์˜ํ•  ์‹ ๊ทœ ๋„คํŠธ์›Œํ‚น ์„œ๋น„์Šค ๊ธฐํš/๊ฐœ๋ฐœ/์šด์˜ +- ์˜คํ”„๋ผ์ธ ํ–‰์‚ฌ ์ฐธ์—ฌ์ž ๊ฐ„ ์ž์—ฐ์Šค๋Ÿฌ์šด ๊ต๋ฅ˜๋ฅผ ๋•๋Š” ๊ฒฝํ—˜ ์„ค๊ณ„ +- ๊ฐ€์งœ์—ฐ๊ตฌ์†Œ ์ˆ˜๋ฃŒ์ฆ ๋ฐœ๊ธ‰ ์‹œ์Šคํ…œ ๋ฆด๋ฆฌ์ฆˆ +- DevFactory x JobPT ์„œ๋น„์Šค ๋ฆด๋ฆฌ์ฆˆ + +## ๐Ÿš€ ํ”„๋กœ์ ํŠธ ๋กœ๋“œ๋งต (Project Roadmap) +```mermaid +gantt + title 2025 DevFactory ์‹ ๊ทœ ๋„คํŠธ์›Œํ‚น ์„œ๋น„์Šค ์—ฌ์ • + dateFormat YYYY-MM-DD + section ํ•ต์‹ฌ ๋งˆ์ผ์Šคํ†ค + ์„œ๋น„์Šค ๊ธฐํš ๋ฐ ์‹œ์Šคํ…œ ์„ค๊ณ„ :a1, 2025-09-08, 4w + โœจ1st Magical Week :b1, 2025-10-06, 1w + ๊ฐœ๋ฐœ ์ดˆ๊ธฐ ๋‹จ๊ณ„ ๋ฐ ํ™˜๊ฒฝ ๊ตฌ์ถ• :a2, 2025-10-13, 3w + โœจ2nd Magical Week :b2, 2025-11-03, 1w + ์ง‘์ค‘ ๊ฐœ๋ฐœ ๋ฐ ๊ธฐ๋Šฅ ๊ณ ๋„ํ™” :a3, 2025-11-10, 7w + ์ตœ์ข… QA ๋ฐ ํ–‰์‚ฌ ์ค€๋น„ :a4, 2025-12-29, 1w + 2nd Grand Gathering ์šด์˜ :milestone, 2026-01-10, 1d + ํ”„๋กœ์ ํŠธ ํšŒ๊ณ  ๋ฐ ๋งˆ๋ฌด๋ฆฌ :a5, 2026-01-12, 2w +``` + +## ๐Ÿ’ป ์ฃผ์ฐจ๋ณ„ ํ™œ๋™ (Activity History) + +| ๋‚ ์งœ | ๋‚ด์šฉ | ๋ฐœํ‘œ์ž | +| :--- | :--- | :---: | +| 2025/09/08 | OT & DevFactory ์†Œ๊ฐœ | DevFactory | +| 2025/09/15 | ์•„์ด๋””์–ด ๋ธŒ๋ ˆ์ธ์Šคํ† ๋ฐ | DevFactory | +| 2025/09/22 | ๋„คํŠธ์›Œํ‚น ์•„์ด๋””์–ด ๊ตฌ์ฒดํ™” & Bingo ๊ธฐํš | DevFactory | +| 2025/09/29 | ์‹œ์Šคํ…œ ์„ค๊ณ„ ๋ฐ ๋ชฉํ‘œ ์ˆ˜๋ฆฝ | DevFactory | +| 2025/10/06 | โœจ1st Magical Week | - | +| 2025/10/13 | UI ๊ณต์œ  ๋ฐ ๊ฐœ๋ฐœ ๋ชฉํ‘œ ์ˆ˜๋ฆฝ | DevFactory | +| 2025/10/20 | ์ž‘์—… ๋‚ด์šฉ ๊ณต์œ  ๋ฐ ๊ฐœ๋ฐœ ๋…ผ์˜ | DevFactory | +| 2025/10/27 | FE-BE ์ดˆ๊ธฐ ํ†ต์‹  ํ™˜๊ฒฝ ๊ตฌ์ถ• ๋ฐ ์ž‘์—… ์ง„ํ–‰ | DevFactory | +| 2025/11/03 | โœจ2nd Magical Week | - | +| 2025/11/10 | ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ์งˆ๋ฌธ ์„ธํŠธ ๊ตฌ์„ฑ | DevFactory | +| 2025/11/17 | ์šด์˜ ์‹œ๋‚˜๋ฆฌ์˜ค ์ˆ˜๋ฆฝ ๋ฐ ์ผ์ • ๋งˆ๊ฐ | DevFactory | +| 2025/11/24 | ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ฐ ์ƒํ˜ธ ๋ฆฌ๋ทฐ | DevFactory | +| 2025/12/01 | ํ”ผ๋“œ๋ฐฑ ๊ธฐ๋ฐ˜ UI/UX ์„ธ๋ถ€ ๊ฐœ์„  | DevFactory | +| 2025/12/08 | ์–ด๋“œ๋ฏผ ๊ธฐ๋Šฅ ๊ธฐํš ๋ฐ ์‹œ์Šคํ…œ ์—ฐ๋™ | DevFactory | +| 2025/12/15 | ์ตœ์ข… ์•ˆ์ •ํ™” ๋ฐ ์˜ˆ์™ธ ์ผ€์ด์Šค ์ฒ˜๋ฆฌ | DevFactory | +| 2025/12/22 | ์šด์˜ ํ™˜๊ฒฝ ๋ฐฐํฌ ๋ฐ ์ตœ์ข… ์ ๊ฒ€ | DevFactory | +| 2025/12/29 | ์ตœ์ข… QA ๋ฐ ํ–‰์‚ฌ ์ค€๋น„ | DevFactory | +| 2026/01/10 | 2nd Grand Gathering - ๋„คํŠธ์›Œํ‚น ์„œ๋น„์Šค ์šด์˜ | DevFactory | +| 2026/01/12 | 11๊ธฐ ํšŒ๊ณ  (2nd GG ํ”ผ๋“œ๋ฐฑ) | DevFactory | +| 2026/01/27 | ๋งˆ๋ฌด๋ฆฌ ํšŒ์‹ | DevFactory | + + + +## ๐Ÿ’ก ํ•™์Šต ์ž์› (Learning Resources) +**์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ์ง€์‹ ํ—ˆ๋ธŒ** +- [๊ฐ€์งœ์—ฐ๊ตฌ์†Œ ์ˆ˜๋ฃŒ์ฆ ๋ฐœ๊ธ‰ ์‹œ์Šคํ…œ](https://cert.pseudo-lab.com/) +- [์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ](https://getcloser.pseudolab-devfactory.com/) +- [JobPT service](https://jobpt.pseudolab-devfactory.com/) \ No newline at end of file diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\353\247\210\353\254\264\353\246\254_\354\235\230\352\262\254.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\353\247\210\353\254\264\353\246\254_\354\235\230\352\262\254.png" new file mode 100644 index 0000000..4c01f52 Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\353\247\210\353\254\264\353\246\254_\354\235\230\352\262\254.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\353\271\231\352\263\240_\354\260\270\354\227\254\354\227\254\353\266\200.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\353\271\231\352\263\240_\354\260\270\354\227\254\354\227\254\353\266\200.png" new file mode 100644 index 0000000..bb42d97 Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\353\271\231\352\263\240_\354\260\270\354\227\254\354\227\254\353\266\200.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\353\271\231\352\263\240\354\231\200\354\235\230\353\271\204\352\265\220.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\353\271\231\352\263\240\354\231\200\354\235\230\353\271\204\352\265\220.png" new file mode 100644 index 0000000..9514b54 Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\353\271\231\352\263\240\354\231\200\354\235\230\353\271\204\352\265\220.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\353\271\231\352\263\240\354\231\200\354\235\230\353\271\204\352\265\220_\354\235\264\354\234\240.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\353\271\231\352\263\240\354\231\200\354\235\230\353\271\204\352\265\220_\354\235\264\354\234\240.png" new file mode 100644 index 0000000..72d8ec6 Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\353\271\231\352\263\240\354\231\200\354\235\230\353\271\204\352\265\220_\354\235\264\354\234\240.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\354\247\210\353\254\270_\353\202\234\354\235\264\353\217\204.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\354\247\210\353\254\270_\353\202\234\354\235\264\353\217\204.png" new file mode 100644 index 0000000..0b6986f Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\354\247\210\353\254\270_\353\202\234\354\235\264\353\217\204.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\354\247\210\353\254\270_\353\217\204\354\233\200\353\217\204.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\354\247\210\353\254\270_\353\217\204\354\233\200\353\217\204.png" new file mode 100644 index 0000000..5e2d073 Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\354\247\210\353\254\270_\353\217\204\354\233\200\353\217\204.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\353\247\214\354\241\261\353\217\204.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\353\247\214\354\241\261\353\217\204.png" new file mode 100644 index 0000000..084f915 Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\353\247\214\354\241\261\353\217\204.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\225\204\354\211\254\354\232\264\354\240\220.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\225\204\354\211\254\354\232\264\354\240\220.png" new file mode 100644 index 0000000..a0721c3 Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\225\204\354\211\254\354\232\264\354\240\220.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\240\201\352\267\271\353\217\204.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\240\201\352\267\271\353\217\204.png" new file mode 100644 index 0000000..571eaa8 Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\240\201\352\267\271\353\217\204.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\242\213\354\225\230\353\215\230\354\240\220.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\242\213\354\225\230\353\215\230\354\240\220.png" new file mode 100644 index 0000000..3e5483e Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\242\213\354\225\230\353\215\230\354\240\220.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\260\270\354\227\254\354\227\254\353\266\200.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\260\270\354\227\254\354\227\254\353\266\200.png" new file mode 100644 index 0000000..4ce8d8f Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\354\271\234\355\225\264\354\247\200\352\270\270\353\260\224\353\235\274_\354\260\270\354\227\254\354\227\254\353\266\200.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\355\214\200\352\265\254\354\204\261_\353\214\200\355\231\224.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\355\214\200\352\265\254\354\204\261_\353\214\200\355\231\224.png" new file mode 100644 index 0000000..6010e7b Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\355\214\200\352\265\254\354\204\261_\353\214\200\355\231\224.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\355\214\200\352\265\254\354\204\261_\353\217\204\354\233\200\353\217\204.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\355\214\200\352\265\254\354\204\261_\353\217\204\354\233\200\353\217\204.png" new file mode 100644 index 0000000..44f7891 Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\355\214\200\352\265\254\354\204\261_\353\217\204\354\233\200\353\217\204.png" differ diff --git "a/docs/imgs/2nd_GG_GetCloser_Review/\355\226\245\355\233\204_\354\260\270\354\227\254\354\227\254\353\266\200.png" "b/docs/imgs/2nd_GG_GetCloser_Review/\355\226\245\355\233\204_\354\260\270\354\227\254\354\227\254\353\266\200.png" new file mode 100644 index 0000000..11c225c Binary files /dev/null and "b/docs/imgs/2nd_GG_GetCloser_Review/\355\226\245\355\233\204_\354\260\270\354\227\254\354\227\254\353\266\200.png" differ diff --git a/docs/imgs/cert-system.png b/docs/imgs/cert-system.png new file mode 100644 index 0000000..9eb02dd Binary files /dev/null and b/docs/imgs/cert-system.png differ diff --git a/docs/imgs/members/seungkyu.jpg b/docs/imgs/members/seungkyu.jpg deleted file mode 100644 index f397988..0000000 Binary files a/docs/imgs/members/seungkyu.jpg and /dev/null differ diff --git a/docs/results/2025-10-25_3rd_Product_DNA_Open_Forum.md b/docs/results/2025-10-25_3rd_Product_DNA_Open_Forum.md new file mode 100644 index 0000000..9787936 --- /dev/null +++ b/docs/results/2025-10-25_3rd_Product_DNA_Open_Forum.md @@ -0,0 +1,57 @@ +# ๐ŸŽ‰ 2025 3rd Product DNA Open Forum + +## ๐Ÿ“Œ ์ „์ฒด ๊ฐœ์š” + +- **๋น™๊ณ  ์ด๋ฒคํŠธ ์ฐธ์—ฌ์ž**: 44๋ช… +- **์ด ํ‚ค์›Œ๋“œ ๊ตํ™˜ ํšŸ์ˆ˜**: 334ํšŒ +- **๋ฆฌ๋ทฐ ์ฐธ์—ฌ์ž**: 11๋ช… +- **๋ฆฌ๋ทฐ ํ‰๊ท  ์ ์ˆ˜**: 5/5 +- **๋ฆฌ๋ทฐ ์ฐธ์—ฌ์œจ**: ์•ฝ **25%** +- **์šด์˜ ์‹œ๊ฐ„**: 1H + + +--- + +## ๐ŸŽฏ ๋น™๊ณ  ๋‹ฌ์„ฑ ํ†ต๊ณ„ + +| ๋‹ฌ์„ฑ ์ค„ ์ˆ˜ | ์ธ์› ์ˆ˜ | +|------------|---------| +| **3์ค„ ์ด์ƒ** | 13๋ช… | +| **2์ค„** | 6๋ช… | +| **1์ค„** | 8๋ช… | + +> ๐Ÿ’ฌ ์ฐธ๊ฐ€์ž์˜ ์•ฝ **30%๊ฐ€ 3์ค„ ์ด์ƒ ๋น™๊ณ ๋ฅผ ๋‹ฌ์„ฑ** + +--- + +## ๐Ÿ—๏ธ ํ‚ค์›Œ๋“œ๋ณ„ ์„ ํƒ ํšŸ์ˆ˜ + +| ์ˆœ์œ„ | ํ‚ค์›Œ๋“œ | ์„ ํƒ ํšŸ์ˆ˜ | +|------|----------------------|-----------| +| 1 | AI Agent | 16 | +| 2 | AI ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค | 11 | +| 3 | ๋น™๊ณ  ํ”ผ๋“œ๋ฐฑ | 11 | +| 4 | ์ธ๊ณผ์ถ”๋ก  | 10 | +| 5 | ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์˜์‚ฌ๊ฒฐ์ • | 10 | +| 6 | ์˜คํ”ˆ์†Œ์Šค ๊ธฐ์—ฌ | 9 | +| 7 | MCP | 8 | +| 8 | Lang2 SQL | 8 | +| 9 | ๋ณด๋“œ๊ฒŒ์ž„ ๋งค๋‹ˆ์•„ | 7 | +| 10 | ๋ฐ์ดํ„ฐ ์—”์ง€๋‹ˆ์–ด๋ง | 6 | +| 11 | ์—ฐ์‚ฌ์ž | 6 | +| 12 | ์—”๋“œํˆฌ์—”๋“œ ์ œํ’ˆ ๊ฐœ๋ฐœ | 5 | +| 13 | ์ถ”์ฒœ ์‹œ์Šคํ…œ | 5 | +| 14 | Lang chain | 5 | +| 15 | A/B ํ…Œ์ŠคํŠธ | 4 | +| 16 | ํ”„๋กœ๋•ํŠธ ์• ๋„๋ฆฌํ‹ฑ์Šค | 4 | + +> ๐Ÿงฉ ๋ณด๋“œ์—์„œ ์„ ํƒ๋œ ํ‚ค์›Œ๋“œ ๊ธฐ์ค€์œผ๋กœ **AI Agent**, **AI ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค**, **๋น™๊ณ  ํ”ผ๋“œ๋ฐฑ** ๋“ฑ์ด ์ƒ์œ„๊ถŒ์„ ๊ธฐ๋กํ•จ + +--- + +## ๐Ÿ” ์š”์•ฝ + +- ์ „์ฒด์ ์œผ๋กœ ์ ์€ ์ธ์›์ด์—ˆ์ง€๋งŒ **ํ™œ๋ฐœํ•œ ํ‚ค์›Œ๋“œ ๊ตํ™˜**์ด ์ด๋ฃจ์–ด์ง„ ํ–‰์‚ฌ์˜€์Œ +- ๋ฆฌ๋ทฐ ์ฐธ์—ฌ์ž ํ‰๊ท  ์ ์ˆ˜๋Š” **5์ **์œผ๋กœ, **์ฐธ๊ฐ€์ž ๋งŒ์กฑ๋„ ์—ญ์‹œ ๋งค์šฐ ๋†’๊ฒŒ ๋‚˜ํƒ€๋‚จ** +- ๋ฐœํ‘œ ์‹œ๊ฐ„์— ์ง์ ‘ ๋ฆฌ๋ทฐ ์ฐธ์—ฌ๋ฅผ ๋…๋ คํ–ˆ๋”๋‹ˆ ๋ฆฌ๋ทฐ ์ฐธ์—ฌ์œจ์ด ๋†’์•˜์Œ. +- ์„œ๋น„์Šค๋ฅผ ์ฒ˜์Œ ์‚ฌ์šฉํ•ด๋ณด๋Š” ์œ ์ €๊ฐ€ ๋งŽ์•˜๋Š”๋ฐ, ๋„คํŠธ์›Œํ‚น์„ ์‹œ์ž‘ํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ค€๋‹ค๋Š” ๊ธ์ •์ ์ธ ๋ฐ˜์‘์ด ๋งŽ์•˜์Œ. \ No newline at end of file diff --git a/docs/results/2026-01-10_2nd_Grand_Gathering.md b/docs/results/2026-01-10_2nd_Grand_Gathering.md new file mode 100644 index 0000000..c874ca3 --- /dev/null +++ b/docs/results/2026-01-10_2nd_Grand_Gathering.md @@ -0,0 +1,134 @@ +# ๐ŸŽ‰ 2nd Grand Gathering + +## ๐Ÿ“Œ ์ „์ฒด ๊ฐœ์š” +- **์ „์ฒด ์‚ฌ์šฉ์ž**: 147๋ช… +- **ํŒ€ ์‹œ๋„**: 11๊ฐœ (ACTIVE 7, CANCELLED 4) +- **ํŒ€ ์ฐธ์—ฌ์ž**: 35๋ช… (์ „์ฒด ๋Œ€๋น„ 23.8%) +- **ํŒ€ ์„ฑ๋ฆฝ(5๋ช… ํ•„์ˆ˜)**: 6ํŒ€ (์„ฑ๋ฆฝ๋ฅ  54.5%) +- **๋ฏธ์„ฑ๋ฆฝ ํŒ€**: 5ํŒ€ (CANCELLED 4, ACTIVE ๋ฏธ์„ฑ๋ฆฝ 1) +- **์ฑŒ๋ฆฐ์ง€ ์ œ์ถœ**: 34๊ฑด +- **์ •๋‹ต**: 33๊ฑด (์ •๋‹ต๋ฅ  97.1%) +- **๊ตฟ์ฆˆ ์ˆ˜๋ น**: 31๊ฑด (์ •๋‹ต ๋Œ€๋น„ 93.9%, ์ œ์ถœ ๋Œ€๋น„ 91.2%) +- **์šด์˜ ์‹œ๊ฐ„**: 2H + +--- + +## ๐ŸŽฏ ํŒ€ ์„ฑ๋ฆฝ ํ˜„ํ™ฉ + +| ๊ตฌ๋ถ„ | ํŒ€ ์ˆ˜ | ๋น„๊ณ  | +|------|------|------| +| ์„ฑ๋ฆฝ | 6 | ํ™•์ • 5๋ช… ์ถฉ์กฑ | +| ๋ฏธ์„ฑ๋ฆฝ | 1 | ACTIVE ๋ฏธ์„ฑ๋ฆฝ 1ํŒ€ (ํŒ€ 9) | +| ์ทจ์†Œ | 4 | CANCELLED 4ํŒ€ | + +> ๐Ÿ’ฌ **ํŒ€ ์„ฑ๋ฆฝ์€ 5๋ช… ํ™•์ •์ด ํ•„์ˆ˜**์ด๋ฉฐ, ๋ฏธ์„ฑ๋ฆฝ/์ทจ์†Œ ํŒ€ ๋น„์ค‘์ด 45.5%๋กœ ๋ณ‘๋ชฉ ๊ตฌ๊ฐ„์œผ๋กœ ๋ณด์ž„ + +--- + +## ๐Ÿงฉ ์ฑŒ๋ฆฐ์ง€ ์ง„ํ–‰ ํ˜„ํ™ฉ +- **์‚ฌ์ „ ์กฐ์‚ฌ**: ์‹ ์ฒญ ์‹œ 1์ธ 3๋ฌธํ•ญ ๊ณ ์ • ์กฐ์‚ฌ +- **์ œ์ถœ**: 34๊ฑด +- **์ •๋‹ต**: 33๊ฑด (์„ค๊ณ„์ƒ 100% ๋ชฉํ‘œ, ์‹ค์ œ 97.1%) +- **๊ตฟ์ฆˆ ์ˆ˜๋ น**: 31๊ฑด (์ •๋‹ต ๋Œ€๋น„ 93.9%) +- **์žฌ์‹œ๋„**: 9๊ฑด + +--- + +## ๐Ÿ—๏ธ ํ‚ค์›Œ๋“œ๋ณ„ ์‘๋‹ต ํšŸ์ˆ˜ (์‚ฌ์ „ ์กฐ์‚ฌ) + +### ๊ด€์‹ฌ ๋ถ„์•ผ +| ์ˆœ์œ„ | ํ‚ค์›Œ๋“œ | ํšŸ์ˆ˜ | ๋น„์œจ | +|------|--------|------|------| +| 1 | Agentic AI | 72 | 49.0% | +| 2 | LLM/Multimodal | 24 | 16.3% | +| 3 | Physical AI | 17 | 11.6% | +| 4 | Causal Inference | 15 | 10.2% | +| 5 | Computer Vision | 6 | 4.1% | +| 6 | XAI | 4 | 2.7% | +| 7 | Efficient AI | 4 | 2.7% | +| 8 | AI Security | 2 | 1.4% | +| 9 | AI Ethics | 2 | 1.4% | +| 10 | Computer Graphics | 1 | 0.7% | + +### ์„ ํ˜ธ ๊ณ„์ ˆ +| ์ˆœ์œ„ | ํ‚ค์›Œ๋“œ | ํšŸ์ˆ˜ | ๋น„์œจ | +|------|--------|------|------| +| 1 | ๊ฐ€์„ | 65 | 44.2% | +| 2 | ๊ฒจ์šธ | 35 | 23.8% | +| 3 | ๋ด„ | 32 | 21.8% | +| 4 | ์—ฌ๋ฆ„ | 15 | 10.2% | + +### MBTI +| ์ˆœ์œ„ | ํ‚ค์›Œ๋“œ | ํšŸ์ˆ˜ | ๋น„์œจ | +|------|--------|------|------| +| 1 | INTP | 18 | 12.2% | +| 2 | ISTJ | 16 | 10.9% | +| 3 | INFJ | 15 | 10.2% | +| 4 | ENTJ | 14 | 9.5% | +| 5 | ENTP | 12 | 8.2% | +| 6 | INFP | 12 | 8.2% | +| 7 | INTJ | 10 | 6.8% | +| 8 | ENFJ | 10 | 6.8% | +| 9 | ESTJ | 10 | 6.8% | +| 10 | ISFJ | 9 | 6.1% | +| 11 | ENFP | 7 | 4.8% | +| 12 | ISFP | 5 | 3.4% | +| 13 | ISTP | 4 | 2.7% | +| 14 | ESFJ | 2 | 1.4% | +| 15 | ESFP | 2 | 1.4% | +| 16 | ESTP | 1 | 0.7% | + +> ๐Ÿ’ฌ ๊ด€์‹ฌ ๋ถ„์•ผ๋Š” **Agentic AI**๊ฐ€ ์ ˆ๋ฐ˜ ๊ฐ€๊นŒ์ด๋กœ ๊ฐ€์žฅ ๋†’์•˜๊ณ , MBTI๋Š” ์ƒ์œ„ ์œ ํ˜•์ด ๊ณ ๋ฅด๊ฒŒ ๋ถ„ํฌ๋จ + +--- + +## ๐Ÿงญ MBTI๋ณ„ ํŒ€ ์ฐธ์—ฌ์œจ +- ํŒ€ ์ฐธ์—ฌ ๊ธฐ์ค€: `team_members`์— ์กด์žฌํ•˜๋Š” ์‚ฌ์šฉ์ž +- MBTI๋Š” ์‚ฌ์ „ ์กฐ์‚ฌ ์‘๋‹ต ๊ธฐ์ค€ + +### E/I ๋น„๊ต +| ๊ตฌ๋ถ„ | ์ „์ฒด ์ธ์› | ํŒ€ ์ฐธ์—ฌ ์ธ์› | ์ฐธ์—ฌ์œจ | +|------|-----------|--------------|--------| +| E | 57 | 18 | 31.6% | +| I | 88 | 17 | 19.3% | + +### MBTI๋ณ„ ์ฐธ์—ฌ์œจ +| MBTI | ์ „์ฒด ์ธ์› | ํŒ€ ์ฐธ์—ฌ ์ธ์› | ์ฐธ์—ฌ์œจ | +|------|-----------|--------------|--------| +| ENFJ | 10 | 3 | 30.0% | +| ENFP | 7 | 2 | 28.6% | +| ENTJ | 13 | 3 | 23.1% | +| ENTP | 12 | 2 | 16.7% | +| ESFJ | 2 | 1 | 50.0% | +| ESFP | 2 | 1 | 50.0% | +| ESTJ | 10 | 5 | 50.0% | +| ESTP | 1 | 1 | 100.0% | +| INFJ | 15 | 4 | 26.7% | +| INFP | 12 | 0 | 0.0% | +| INTJ | 10 | 4 | 40.0% | +| INTP | 18 | 3 | 16.7% | +| ISFJ | 9 | 2 | 22.2% | +| ISFP | 5 | 0 | 0.0% | +| ISTJ | 15 | 4 | 26.7% | +| ISTP | 4 | 0 | 0.0% | + +> ๐Ÿ’ฌ ํ‘œ๋ณธ์ด ์ž‘์€ ์œ ํ˜•(์˜ˆ: ESFP, ESTP)์€ ๋น„์œจ ๋ณ€๋™์„ฑ์ด ํฌ๋ฏ€๋กœ ํ•ด์„์— ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•จ + +--- + +## โฑ๏ธ ์‹œ๊ฐ„ ๋ฒ”์œ„ +- **ํŒ€ ์ƒ์„ฑ**: 2026-01-10 14:55:59 ~ 16:23:33 +- **์ฑŒ๋ฆฐ์ง€ ์ œ์ถœ**: 2026-01-10 14:57:17 ~ 16:25:16 +- **๊ตฟ์ฆˆ ์ˆ˜๋ น**: 2026-01-10 14:58:08 ~ 16:54:01 + +--- + +## ๐Ÿ” ์š”์•ฝ +- ํŒ€ ์ฐธ์—ฌ ์ „ํ™˜์ด ๋‚ฎ์•„ ํŒ€ ์„ฑ๋ฆฝ(5๋ช… ํ™•์ •)๊นŒ์ง€ ๊ฐ€๋Š” ๊ณผ์ •์ด ํ•ต์‹ฌ ๋ณ‘๋ชฉ์œผ๋กœ ๋ณด์ž„ +- CANCELLED/๋ฏธ์„ฑ๋ฆฝ ํŒ€์ด 45.5%๋กœ, ํŒ€ ๊ตฌ์„ฑ ๋‹จ๊ณ„์—์„œ ์ดํƒˆ์ด ์ง‘์ค‘๋จ +- MBTI ๊ธฐ์ค€์œผ๋กœ๋Š” E ์„ฑํ–ฅ์ด I ์„ฑํ–ฅ๋ณด๋‹ค ํŒ€ ์ฐธ์—ฌ์œจ์ด ๋†’๊ฒŒ ๋‚˜ํƒ€๋‚จ (ํ‘œ๋ณธ ํ•œ๊ณ„ ์กด์žฌ) + +--- + +## ๐Ÿ› ๏ธ ๋‹ค์Œ ํ–‰์‚ฌ ์ค€๋น„ ํฌ์ธํŠธ +- **ํŒ€ ์ƒ์„ฑ ์ด‰์ง„**: ํŒ€ ์ธ์›์„ ๊ฐ์ถ• ๋“ฑ, ๋ณด๋‹ค ์‰ฝ๊ฒŒ ํŒ€์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์•ˆ ๋งˆ๋ จ diff --git a/docs/results/2026-01-10_2nd_Grand_Gathering_GetCloser_Review.md b/docs/results/2026-01-10_2nd_Grand_Gathering_GetCloser_Review.md new file mode 100644 index 0000000..afaaa1a --- /dev/null +++ b/docs/results/2026-01-10_2nd_Grand_Gathering_GetCloser_Review.md @@ -0,0 +1,128 @@ +# 2nd Grand Gathering GetCloser Review + +## 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š” + +- **ํ”„๋กœ์ ํŠธ๋ช…:** ์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ (GetCloser) +- **๋ชฉ์ :** ์˜คํ”„๋ผ์ธ ํ–‰์‚ฌ ์ฐธ์—ฌ์ž ๊ฐ„ ์‹ฌ๋ฆฌ์  ๊ฑฐ๋ฆฌ ๊ฐ์†Œ ๋ฐ ๋„คํŠธ์›Œํ‚น ํ™œ์„ฑํ™” +- **์šด์˜ ์ผ์‹œ:** 2026.01.10 (2nd Grand Gathering) +- **์šด์˜ ๊ทœ๋ชจ:** ํ–‰์‚ฌ ์ฐธ์—ฌ์ž 200๋ช… ์ด์ƒ + +## 2. ์„ค๋ฌธ ๋ฐ ์ฐธ์—ฌ ํ˜„ํ™ฉ + +- **์„ค๋ฌธ ์‘๋‹ต:** ์ด 22๋ช… (์‹ค์ œ ์„œ๋น„์Šค ์ด์šฉ์ž 11๋ช…, ๋ฏธ์ด์šฉ์ž 11๋ช…) +- **์„œ๋น„์Šค ๋„๋‹ฌ๋ฅ :** ํ–‰์‚ฌ ์‹œ๋„์ž(147๋ช…) ๋Œ€๋น„ ์„ค๋ฌธ ์ฐธ์—ฌ ์‹ค์‚ฌ์šฉ์ž(11๋ช…) ๊ธฐ์ค€, ์•ฝ 7%์˜ ์‹ฌ์ธต ํ”ผ๋“œ๋ฐฑ ํ™•๋ณด + +## 3. ์ฃผ์š” ์ง€ํ‘œ (Key Metrics) + +- **์„œ๋น„์Šค ์ „๋ฐ˜ ๋งŒ์กฑ๋„:** โญ **4.27 / 5.0** +- **๊ธฐ์กด ๋ฐฉ์‹(๋น™๊ณ ) ๋Œ€๋น„ ์„ ํ˜ธ๋„:** **3.33 / 5.0** +- **์žฌ์ฐธ์—ฌ ์˜์‚ฌ:** **100% ๊ธ์ •** (๊ผญ ์ฐธ์—ฌํ•˜๊ณ  ์‹ถ๋‹ค 72.5%, ์ƒํ™ฉ์— ๋”ฐ๋ผ ์ฐธ์—ฌ 27.3%) + +## 4. ์ƒ์„ธ ๋ถ„์„ ๊ฒฐ๊ณผ + +### 4-1. ์ด๋ฒˆ ๋„คํŠธ์›Œํ‚น(์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ) ์ฐธ์—ฌ ์—ฌ๋ถ€ + +![](../imgs/2nd_GG_GetCloser_Review/์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ_์ฐธ์—ฌ์—ฌ๋ถ€.png) + +- ํ–‰์‚ฌ ๋‚ด ์ด 147๋ช…์ด ์„œ๋น„์Šค ์ด์šฉ์„ ์‹œ๋„ํ•˜์˜€์œผ๋ฉฐ, ๊ทธ์ค‘ 36๋ช…์ด ๋ฏธ์…˜์„ ์ตœ์ข… ์„ฑ๊ณต +- ์„ค๋ฌธ์—๋Š” ์‹ค์ œ ์„ฑ๊ณต ์ธ์› ์ค‘ 11๋ช…์ด ์ฐธ์—ฌํ•˜์—ฌ, ์„œ๋น„์Šค ๊ฒฝํ—˜์— ๋Œ€ํ•œ ์‹ฌ์ธต์ ์ธ ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต + +### 4-2. ๊ธฐ์กด๋ฐฉ์‹(๋น™๊ณ )๊ณผ์˜ ๋น„๊ต + +**์ฐธ์—ฌ ์—ฌ๋ถ€** + +![](../imgs/2nd_GG_GetCloser_Review/๋น™๊ณ _์ฐธ์—ฌ์—ฌ๋ถ€.png) + +- ์ด๋ฒˆ ๋„คํŠธ์›Œํ‚น ์ฐธ์—ฌ์ž 11๋ช… ์ค‘ 54.5%(6๋ช…)๊ฐ€ ์ด์ „ ๋น™๊ณ  ๋ฐฉ์‹์˜ ๋„คํŠธ์›Œํ‚น์„ ๊ฒฝํ—˜ํ•ด ๋ณธ ๊ฒƒ์œผ๋กœ ํ™•์ธ๋จ. + +**๊ธฐ์กด ๋น™๊ณ  ๋„คํŠธ์›Œํ‚น ๋น„๊ต** + +![](../imgs/22nd_GG_GetCloser_Review/๋น™๊ณ ์™€์˜๋น„๊ต.png) + +- ํ‰๊ท  3.33/5.0์ ์œผ๋กœ, ์ด๋ฒˆ ์„œ๋น„์Šค๊ฐ€ ๊ธฐ์กด ๋ฐฉ์‹๋ณด๋‹ค ์••๋„์ ์œผ๋กœ ์šฐ์ˆ˜ํ•˜๋‹ค๊ณ  ๋‹จ์ •ํ•˜๊ธฐ๋Š” ์–ด๋ ค์šฐ๋‚˜ ์ƒˆ๋กœ์šด ๋ฐฉ์‹์— ๋Œ€ํ•œ ์œ ์˜๋ฏธํ•œ ํ”ผ๋“œ๋ฐฑ์„ ํ™•๋ณดํ•จ. + +**๊ทธ๋ ‡๊ฒŒ ๋А๋‚€ ์ด์œ ** + +![](../imgs/2nd_GG_GetCloser_Review/๋น™๊ณ ์™€์˜๋น„๊ต_์ด์œ .png) + +- "๋‘ ๋ฐฉ์‹ ๊ฐ„ ํฐ ์ฐจ์ด๋ฅผ ๋А๋ผ์ง€ ๋ชปํ–ˆ๋‹ค", "๋ฐฉ์‹๋ณด๋‹ค๋Š” ๊ฐœ์ธ์˜ ์ ๊ทน์„ฑ์— ๋”ฐ๋ผ ๊ฒฝํ—˜์ด ๋‹ฌ๋ผ์ง„๋‹ค"๋Š” ์ค‘๋ฆฝ์  ์˜๊ฒฌ์ด ์กด์žฌํ•จ. +- ๊ธ์ •์ ์œผ๋กœ๋Š” "ํŠน์ • ์ธ์›๋“ค๊ณผ ๋Œ€ํ™”๋ฅผ ์ด์–ด๊ฐ€๋ฉฐ ๊ด€๊ณ„๊ฐ€ ํ˜•์„ฑ๋˜๋Š” ๋А๋‚Œ"์„ ์•˜์œผ๋‚˜, "๋น™๊ณ  ๋ฐฉ์‹์ด ๋” ๋งŽ์€ ์‚ฌ๋žŒ์„ ์ž์œ ๋กญ๊ฒŒ ๋งŒ๋‚  ์ˆ˜ ์žˆ์—ˆ๋‹ค"๋Š” ๋น„๊ต ์˜๊ฒฌ๋„ ์žˆ์—ˆ์Œ. + +### 4-3. ์„œ๋น„์Šค ์ „๋ฐ˜ ๋งŒ์กฑ๋„ + +**์„œ๋น„์Šค ์ž์ฒด ๋งŒ์กฑ๋„** + +![](../imgs/2nd_GG_GetCloser_Review/์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ_๋งŒ์กฑ๋„.png) + +- 4.27์ ์œผ๋กœ ์ „์ฒด์ ์œผ๋กœ ๋†’์€ ๋งŒ์กฑ๋„๋ฅผ ๊ธฐ๋กํ•จ. + +**์ ๊ทน์  ์ฐธ์—ฌ๋„** + +![](../imgs/2nd_GG_GetCloser_Review/์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ_์ ๊ทน๋„.png) + +- 5์  ๋งŒ์  ๊ธฐ์ค€ 5์ (6๋ช…), 4์ (2๋ช…)์œผ๋กœ ๋Œ€๋ถ€๋ถ„ ๋งค์šฐ ์—ด์ •์ ์œผ๋กœ ์ฐธ์—ฌํ•จ. + +**์ข‹์•˜๋˜ ์ ** + +![](../imgs/2nd_GG_GetCloser_Review/์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ_์ข‹์•˜๋˜์ .png) + +- "์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋Œ€ํ™”ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค"๋Š” ์˜๊ฒฌ์ด 8๋ช…์œผ๋กœ ๊ฐ€์žฅ ๋งŽ์•˜์Œ. +- ์ด์–ด ๋ถ„์œ„๊ธฐ(7๋ช…), ์„œ๋น„์Šค์˜ ์žฌ๋ฏธ(5๋ช…), ๋ช…ํ™•ํ•œ ์ง„ํ–‰ ๋ฐฉ์‹(3๋ช…) ์ˆœ์œผ๋กœ ๊ธ์ •์ ์ธ ํ‰๊ฐ€๊ฐ€ ์ด์–ด์ง. +- ๊ธฐํƒ€: "๊ด€์‹ฌ ํšŒ์‚ฌ ์‚ฌ๋žŒ๊ณผ ๋Œ€ํ™” ๊ฐ€๋Šฅ", "์‹ค์งˆ์ ์ธ ๋„คํŠธ์›Œํ‚น ํšจ๊ณผ", "๊ท€์—ฌ์šด UI" ๋“ฑ์˜ ๊ฐœ์ธ ์˜๊ฒฌ์ด ์žˆ์—ˆ์Œ. + +**์•„์‰ฌ์› ๋˜ ์ ** + +![](../imgs/2nd_GG_GetCloser_Review/์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ_์•„์‰ฌ์šด์ .png) + +- "์ฃผ์–ด์ง„ ์‹œ๊ฐ„์ด ์งง์•˜๋‹ค(8๋ช…)"๋Š” ์˜๊ฒฌ์ด ๊ฐ€์žฅ ๋งŽ์•„, ํ–‰์‚ฌ ๋‚ด ์‹œ๊ฐ„ ๋ฐฐ๋ถ„ ๊ฐœ์„ ์ด ์‹œ๊ธ‰ํ•จ์„ ํ™•์ธ ํ•จ. +- ๊ทœ์น™ ์ดํ•ด์˜ ์–ด๋ ค์›€(3๋ช…)์ด๋‚˜ ์ฐธ์—ฌ์ž ์ˆ˜ ๋ถ€์กฑ(2๋ช…) ๋“ฑ์— ๋Œ€ํ•œ ํ”ผ๋“œ๋ฐฑ๋„ ์žˆ์—ˆ์Œ. +- **์šด์˜ ๋ณ€์ˆ˜:** "๋ช…์ฐฐ์— ๋‹ต์ด ์ด๋ฏธ ์ธ์‡„๋˜์–ด ์žˆ์–ด ์•„์‰ฌ์› ๋‹ค"๋Š” ์˜๊ฒฌ์„ ํ†ตํ•ด ์šด์˜์ง„๊ณผ ์‚ฌ์ „ ์กฐ์œจ ํ•„์š”์„ฑ์„ ํ™•์ธ + +### 4-4. ์„œ๋น„์Šค ๋‚ด ์งˆ๋ฌธ ์ฝ˜ํ…์ธ  ๋งŒ์กฑ๋„ + +**๋Œ€ํ™” ๋„์›€ ์—ฌ๋ถ€** + +![](../imgs/2nd_GG_GetCloser_Review/์งˆ๋ฌธ_๋„์›€๋„.png) + +- ์งˆ๋ฌธ ์ฝ˜ํ…์ธ ๊ฐ€ ์„œ๋กœ๋ฅผ ์•Œ์•„๊ฐ€๋Š” ๋ฐ ๋„์›€์ด ๋˜์—ˆ๋Š”์ง€์— ๋Œ€ํ•ด 5์ (6๋ช…)์„ ํฌํ•จํ•ด ์ „๋ฐ˜์ ์œผ๋กœ ๋†’์€ ๋งŒ์กฑ๋„๋ฅผ ๋ณด์ž„. (MBTI, AI ๊ด€์‹ฌ์‚ฌ, ์ข‹์•„ํ•˜๋Š” ๊ณ„์ ˆ ๋“ฑ) + +**์งˆ๋ฌธ์˜ ๋‚œ์ด๋„** + +![](../imgs/2nd_GG_GetCloser_Review/์งˆ๋ฌธ_๋‚œ์ด๋„.png) + +- ์ ๋‹นํ–ˆ๋‹ค(54.5%), ์‰ฌ์› ๋‹ค(36.4%)๋Š” ์˜๊ฒฌ์ด ์ฃผ๋ฅผ ์ด๋ฃธ. ๋ช…์ฐฐ์— ์ด๋ฏธ ๋‹ต์ด ๋…ธ์ถœ๋˜์–ด ๋ณ€๋ณ„๋ ฅ์ด ๋‚ฎ์•„์ง„ ์ ์ด ์˜ํ–ฅ์„ ์ค€ ๊ฒƒ์œผ๋กœ ๋ณด์ž„. + +### 4-5. ์„œ๋น„์Šค ๋‚ด ํŒ€ ๊ตฌ์„ฑ ๋งŒ์กฑ + +**ํŒ€ ๊ตฌ์„ฑ ๋งŒ์กฑ๋„** + +![](../imgs/2nd_GG_GetCloser_Review/ํŒ€๊ตฌ์„ฑ_๋„์›€๋„.png) + +- 5์ (4๋ช…), 4์ (6๋ช…)์œผ๋กœ ๋Œ€ํ™” ํ™˜๊ฒฝ์„ ์กฐ์„ฑํ•˜๋Š” ๋ฐ ๋งค์šฐ ๊ธ์ •์ ์ธ ์—ญํ• ์„ ํ•จ + +**๋Œ€ํ™” ํ™œ์„ฑ๋„** + +![](../imgs/2nd_GG_GetCloser_Review/ํŒ€๊ตฌ์„ฑ_๋Œ€ํ™”.png) + +- 81.7%๊ฐ€ ์›ํ™œํ•˜๊ฒŒ ์ด์•ผ๊ธฐ๋ฅผ ๋‚˜๋ˆ„์—ˆ์œผ๋‚˜, 18.2%๋Š” "์ผ๋ถ€ ์‚ฌ๋žŒ ์œ„์ฃผ๋กœ ๋Œ€ํ™”๊ฐ€ ์ง„ํ–‰๋˜์—ˆ๋‹ค"๋Š” ์•„์‰ฌ์›€์„ ํ‘œํ•จ. + +**ํ–ฅํ›„ ์ฐธ์—ฌ ์˜์‚ฌ** + +![](../imgs/2nd_GG_GetCloser_Review/ํ–ฅํ›„_์ฐธ์—ฌ์—ฌ๋ถ€.png) + +- **๊ผญ ์ฐธ์—ฌํ•˜๊ณ  ์‹ถ๋‹ค(72.5%)**, ์ƒํ™ฉ์— ๋”ฐ๋ผ ์ฐธ์—ฌํ•˜๊ฒ ๋‹ค(27.3%)๋กœ ๋งค์šฐ ๊ธ์ •์ ์ธ ์žฌ์ฐธ์—ฌ ์˜์‚ฌ๋ฅผ ํ™•์ธ ํ•จ. + +### 4-6. ๋งˆ๋ฌด๋ฆฌ & ๊ฐœ์„  + +![](../imgs/2nd_GG_GetCloser_Review/๋งˆ๋ฌด๋ฆฌ_์˜๊ฒฌ.png) + +- ์ „๋ฐ˜์ ์œผ๋กœ DevFactory์— ๋Œ€ํ•œ ๊ฐ์‚ฌ์™€ ์„œ๋น„์Šค์˜ ๊ธ์ •์ ์ธ ๋ฉด์— ๋Œ€ํ•œ ์˜๊ฒฌ์ด ๋งŽ์•˜์Œ. +- ์ฃผ๊ด€์‹์—์„œ๋„ "5๋ช… ๋ชจ์œผ๊ธฐ" ๋ฏธ์…˜์˜ ๋‚œ์ด๋„๊ฐ€ ๋†’๋‹ค๋Š” ์˜๊ฒฌ์ด ๋ฐ˜๋ณต๋œ ๋งŒํผ, ์ถ”ํ›„ ์ธ์› ๊ธฐ์ค€์ด๋‚˜ ๋ฏธ์…˜ ๋‹ฌ์„ฑ ๋ฐฉ์‹์„ ๊ฐœ์„ ํ•  ํ•„์š”๊ฐ€ ์žˆ์Œ. + +## 5. ์ „์ฒด ๋ฆฌ๋ทฐ + +์˜ˆ์ƒ๋ณด๋‹ค ๊ฐœ๋ฐœ ์ผ์ • ์ง€์—ฐ๊ณผ ํ–‰์‚ฌ์žฅ ํ™•์ •์ด ๋Šฆ์–ด์ง€๋ฉด์„œ ํ˜„์žฅ์˜ ๊ณต๊ฐ„์  ํŠน์„ฑ์„ ์„œ๋น„์Šค์— ์ถฉ๋ถ„ํžˆ ๋ฐ˜์˜ํ•˜์ง€ ๋ชปํ•œ์ ์ด ์•„์‰ฝ๋‹ค. . ํŠนํžˆ ์ „์ฒด ์ฐธ์—ฌ ์ธ์› ๋Œ€๋น„ ์‹ค์ œ ๋ฏธ์…˜ ์„ฑ๊ณต๊นŒ์ง€ ์ด์–ด์ง„ ๋น„์œจ์ด ๋‚ฎ์•˜๋˜ ์ ์€ ํ–‰์‚ฌ์žฅ ๋ฐฐ์น˜๋‚˜ ์šด์˜ ํ™˜๊ฒฝ์ด ์„œ๋น„์Šค ์ฐธ์—ฌ์— ํ—ˆ๋“ค๋กœ ์ž‘์šฉํ–ˆ์Œ์„ ์‹œ์‚ฌํ•œ๋‹ค. ํ–ฅํ›„ ์šด์˜ ์‹œ์—๋Š” ์‹ค์ œ ์ฐธ์—ฌ์ž ๋น„์œจ์„ ๊ณ ๋ คํ•˜์—ฌ ํŒ€ ๊ตฌ์„ฑ ์ธ์›์„ ์œ ์—ฐํ•˜๊ฒŒ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ์ด ํ•„์š”ํ•ด๋ณด์ธ๋‹ค. ์ „๋ฐ˜์ ์œผ๋กœ ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ๊ธ์ •์ ์ธ ํ‰๊ฐ€๋ฅผ ๋ฐ›์•˜์œผ๋‚˜, ์ดํ›„ ์šด์˜์„ ์œ„ํ•ด ๊ฐœ๋ฐœ ํ”„๋กœ์„ธ์Šค ํšจ์œจํ™”์™€ ํ–‰์‚ฌ์žฅ์˜ ๋ฌผ๋ฆฌ์  ํŠน์„ฑ์„ ๊ณ ๋ คํ•œ ์„œ๋น„์Šค ๊ฐœ์„ ์˜ ํ•„์š”์„ฑ์„ ๋А๊ผˆ๋‹ค. + +## 6. ์„ค๋ฌธ ๊ฐœ๋ณ„ ์‘๋‹ต ๋ณด๊ธฐ + +[์„ค๋ฌธ ๊ฐœ๋ณ„์‘๋‹ต ์—ฐ๊ฒฐ](https://docs.google.com/spreadsheets/d/1wvRDBG1lrp8zL3geO_yi78qEMkYiIdALJEQAyuSkZn4/edit?usp=sharing) \ No newline at end of file diff --git a/getcloser/.env.example b/getcloser/.env.example index bffd04b..ccaaeb6 100644 --- a/getcloser/.env.example +++ b/getcloser/.env.example @@ -2,6 +2,8 @@ DB_USER=user DB_PASSWORD=password DB_DATABASE=app_db +DB_PORT=5432 +# DATA_DIR_HOST=./backend/app/scripts # ํ˜ธ์ŠคํŠธ ์‹œ๋“œ ๋ฐ์ดํ„ฐ ๊ฒฝ๋กœ(์ ˆ๋Œ€/์ƒ๋Œ€ ๋ชจ๋‘ ๊ฐ€๋Šฅ), ์ปจํ…Œ์ด๋„ˆ์—์„œ๋Š” /app/seed-data๋กœ ๊ณ ์ • ๋งˆ์šดํŠธ # === Auth (currently unused) === # SECRET_KEY=secret_key @@ -10,4 +12,8 @@ DB_DATABASE=app_db # === App Host === APP_HOST=localhost TEAM_SIZE=1 -PENDING_TIMEOUT_MINUTES=1 \ No newline at end of file +PENDING_TIMEOUT_MINUTES=1 + +# Swagger Credentials +SWAGGER_USER=your_swagger_username +SWAGGER_PASSWORD=your_swagger_password diff --git a/getcloser/backend/app/api/v1/challenges/challenges.py b/getcloser/backend/app/api/v1/challenges/challenges.py index 427417f..9dd9ea1 100644 --- a/getcloser/backend/app/api/v1/challenges/challenges.py +++ b/getcloser/backend/app/api/v1/challenges/challenges.py @@ -17,7 +17,7 @@ def challenge_retry_controller(request: ChallengeRetryRequest, db: Session = Dep @router.post("/assign", response_model=ChallengeResponse) def assign_challenges(request: ChallengeRequest, db: Session = Depends(get_db)): try: - assigned = assign_challenges_logic(request.my_id, request.members_ids, db) + assigned = assign_challenges_logic(request.my_id, request.members_ids, request.team_id, db) return ChallengeResponse(team_id=request.team_id, my_assigned=assigned) except ValueError as e: diff --git a/getcloser/backend/app/main.py b/getcloser/backend/app/main.py index bc74fe7..070f2e1 100644 --- a/getcloser/backend/app/main.py +++ b/getcloser/backend/app/main.py @@ -1,7 +1,12 @@ -from fastapi import FastAPI +from fastapi import Depends, FastAPI, HTTPException, status from fastapi.middleware.cors import CORSMiddleware +from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html +from fastapi.openapi.utils import get_openapi +from fastapi.responses import JSONResponse +from fastapi.security import HTTPBasic, HTTPBasicCredentials from dotenv import load_dotenv import os +import secrets from core.database import engine, Base from routers import test_db, admin @@ -17,12 +22,14 @@ Base.metadata.create_all(bind=engine) # FastAPI ์•ฑ ์ƒ์„ฑ +is_dev = os.getenv("ENV") == "development" app = FastAPI( title="Devfactory ์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ", description="Devfactory ์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ API ์„œ๋ฒ„", version="1.0.0", - docs_url="/docs", - redoc_url="/redoc", + docs_url="/docs" if is_dev else None, + redoc_url="/redoc" if is_dev else None, + openapi_url="/openapi.json" if is_dev else None, ) @@ -41,6 +48,58 @@ app.include_router(test_db.test_router, prefix="/api/v1") app.include_router(admin.admin_router, prefix="/api/v1") +security = HTTPBasic(auto_error=False) + +def verify_docs_credentials(credentials: HTTPBasicCredentials | None = Depends(security)) -> None: + if is_dev: + return + + expected_user = os.getenv("SWAGGER_USER") + expected_password = os.getenv("SWAGGER_PASSWORD") + + # If credentials aren't configured, keep docs closed by default. + if not expected_user or not expected_password: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Swagger credentials are not configured.", + ) + + if credentials is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Missing credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + + is_user_ok = secrets.compare_digest(credentials.username, expected_user) + is_password_ok = secrets.compare_digest(credentials.password, expected_password) + if not (is_user_ok and is_password_ok): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid credentials", + headers={"WWW-Authenticate": "Basic"}, + ) + +if not is_dev: + @app.get("/docs", include_in_schema=False) + def get_swagger_docs(_: None = Depends(verify_docs_credentials)): + return get_swagger_ui_html(openapi_url="/openapi.json", title="API Docs") + + @app.get("/redoc", include_in_schema=False) + def get_redoc_docs(_: None = Depends(verify_docs_credentials)): + return get_redoc_html(openapi_url="/openapi.json", title="API Docs") + + @app.get("/openapi.json", include_in_schema=False) + def openapi_json(_: None = Depends(verify_docs_credentials)): + return JSONResponse( + get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + ) + @app.get("/") async def read_root(): diff --git a/getcloser/backend/app/models/challenges.py b/getcloser/backend/app/models/challenges.py index a607e2a..0ef02f1 100644 --- a/getcloser/backend/app/models/challenges.py +++ b/getcloser/backend/app/models/challenges.py @@ -6,8 +6,9 @@ class UserChallengeStatus(Base): __tablename__ = "user_challenge_status" - user_id = Column(Integer, primary_key=True, index=True) - challenge_id = Column(Integer, ForeignKey("challenge_questions.id"), index=True) + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + user_id = Column(Integer, index=True) + challenge_id = Column(Integer, ForeignKey("challenge_questions.id"), nullable=True) is_correct = Column(Boolean, default=False) submitted_at = Column(DateTime(timezone=True)) diff --git a/getcloser/backend/app/schemas/challenge_schema.py b/getcloser/backend/app/schemas/challenge_schema.py index fa32c2e..c38c868 100644 --- a/getcloser/backend/app/schemas/challenge_schema.py +++ b/getcloser/backend/app/schemas/challenge_schema.py @@ -21,7 +21,6 @@ class Config: class AssignedChallenge(BaseModel): user_id: int assigned_challenge_id: int - from_user_id: int category: str answer: str diff --git a/getcloser/backend/app/schemas/team_schema.py b/getcloser/backend/app/schemas/team_schema.py index 8e6eb37..e640740 100644 --- a/getcloser/backend/app/schemas/team_schema.py +++ b/getcloser/backend/app/schemas/team_schema.py @@ -30,6 +30,5 @@ class TeamInfoResponse(BaseModel): class MemberChallengeResponse(BaseModel): user_id: int question: str - user_answer: str correct_answer: str is_correct: bool diff --git a/getcloser/backend/app/scripts/challenge_question.csv b/getcloser/backend/app/scripts/challenge_question.csv index 3c2d238..8f2a93b 100644 --- a/getcloser/backend/app/scripts/challenge_question.csv +++ b/getcloser/backend/app/scripts/challenge_question.csv @@ -1,61 +1,76 @@ id,user_id,category,answer -1,1,1,LLM -2,1,2,๋“ฑ์‚ฐ -3,1,3,INFP -4,2,1,AI Agent -5,2,2,์ˆ˜์˜ -6,2,3,ENFJ -7,3,1,CV -8,3,2,ํ•„๋ผํ…Œ์Šค -9,3,3,ISTJ -10,4,1,Multimodal -11,4,2,์š”๊ฐ€ -12,4,3,ESTP -13,5,1,LLM -14,5,2,๋Ÿฌ๋‹ +1,1,1,Physical AI +2,1,2,์—ฌ๋ฆ„ +3,1,3,ENFP +4,2,1,Agentic AI +5,2,2,๊ฒจ์šธ +6,2,3,ISTJ +7,3,1,Causal Inference +8,3,2,๊ฐ€์„ +9,3,3,INTP +10,4,1,LLM/Multimodal +11,4,2,๋ด„ +12,4,3,ENTJ +13,5,1,Computer Vision +14,5,2,์—ฌ๋ฆ„ 15,5,3,ISFP -16,6,1,AI Agent -17,6,2,ํ—ฌ์Šค -18,6,3,ENTP -19,7,1,CV -20,7,2,๋“ฑ์‚ฐ -21,7,3,INFJ -22,8,1,Multimodal -23,8,2,์ˆ˜์˜ -24,8,3,ESFJ -25,9,1,LLM -26,9,2,ํ•„๋ผํ…Œ์Šค -27,9,3,INTJ -28,10,1,AI Agent -29,10,2,์š”๊ฐ€ -30,10,3,ENTJ -31,11,1,CV -32,11,2,๋Ÿฌ๋‹ -33,11,3,INTP -34,12,1,Multimodal -35,12,2,ํ—ฌ์Šค -36,12,3,ESFP -37,13,1,LLM -38,13,2,๋“ฑ์‚ฐ -39,13,3,ISTP -40,14,1,AI Agent -41,14,2,์ˆ˜์˜ -42,14,3,ENFP -43,15,1,CV -44,15,2,ํ•„๋ผํ…Œ์Šค -45,15,3,ISFJ -46,16,1,Multimodal -47,16,2,์š”๊ฐ€ +16,6,1,Agentic AI +17,6,2,๊ฐ€์„ +18,6,3,INFJ +19,7,1,Physical AI +20,7,2,๊ฒจ์šธ +21,7,3,ESTJ +22,8,1,XAI +23,8,2,๋ด„ +24,8,3,INTJ +25,9,1,Agentic AI +26,9,2,์—ฌ๋ฆ„ +27,9,3,ENFJ +28,10,1,Causal Inference +29,10,2,๋ด„ +30,10,3,ISTP +31,11,1,LLM/Multimodal +32,11,2,๊ฐ€์„ +33,11,3,INFP +34,12,1,Physical AI +35,12,2,์—ฌ๋ฆ„ +36,12,3,ENTP +37,13,1,Agentic AI +38,13,2,๊ฒจ์šธ +39,13,3,ISFJ +40,14,1,Computer Graphics +41,14,2,๋ด„ +42,14,3,INTP +43,15,1,Causal Inference +44,15,2,๊ฐ€์„ +45,15,3,ENFP +46,16,1,Agentic AI +47,16,2,์—ฌ๋ฆ„ 48,16,3,ESTJ -49,17,1,LLM -50,17,2,๋Ÿฌ๋‹ -51,17,3,INFP -52,18,1,AI Agent -53,18,2,ํ—ฌ์Šค -54,18,3,ENFJ -55,19,1,CV -56,19,2,๋“ฑ์‚ฐ -57,19,3,ISTJ -58,20,1,Multimodal -59,20,2,์ˆ˜์˜ -60,20,3,ESTP +49,17,1,LLM/Multimodal +50,17,2,๊ฒจ์šธ +51,17,3,INFJ +52,18,1,Physical AI +53,18,2,๋ด„ +54,18,3,ISTJ +55,19,1,Agentic AI +56,19,2,๊ฐ€์„ +57,19,3,ENTJ +58,20,1,XAI +59,20,2,๊ฒจ์šธ +60,20,3,INTP +61,21,1,Computer Vision +62,21,2,์—ฌ๋ฆ„ +63,21,3,ENFJ +64,22,1,Agentic AI +65,22,2,๋ด„ +66,22,3,ISTP +67,23,1,Physical AI +68,23,2,๊ฐ€์„ +69,23,3,INTJ +70,24,1,LLM/Multimodal +71,24,2,๊ฒจ์šธ +72,24,3,INFP +73,25,1,Causal Inference +74,25,2,์—ฌ๋ฆ„ +75,25,3,ENTP diff --git a/getcloser/backend/app/scripts/challenge_question.py b/getcloser/backend/app/scripts/challenge_question.py index c45b515..51289c8 100644 --- a/getcloser/backend/app/scripts/challenge_question.py +++ b/getcloser/backend/app/scripts/challenge_question.py @@ -31,7 +31,3 @@ def seed_challenge_questions_from_csv(file_path: str): finally: db.close() - - -# if __name__ == "__main__": -# seed_challenge_questions_from_csv("./scripts/challenge_question.csv") diff --git a/getcloser/backend/app/scripts/seed.py b/getcloser/backend/app/scripts/seed.py index acc5d2c..d9cd725 100644 --- a/getcloser/backend/app/scripts/seed.py +++ b/getcloser/backend/app/scripts/seed.py @@ -1,14 +1,30 @@ +import os from pathlib import Path from scripts.users import seed_users_from_csv from scripts.challenge_question import seed_challenge_questions_from_csv -BASE_DIR = Path(__file__).resolve().parent +DEFAULT_DATA_DIR = Path(__file__).resolve().parent + +# DATA_DIR ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์žˆ์œผ๋ฉด ๊ฑฐ๊ธฐ๋ฅผ, ์—†์œผ๋ฉด ๊ธฐ๋ณธ scripts ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ์‚ฌ์šฉ +env_data_dir = os.getenv("DATA_DIR") +DATA_DIR = ( + Path(env_data_dir).expanduser().resolve() + if env_data_dir + else DEFAULT_DATA_DIR +) def seed_initial_data(): """ Seed all baseline data sets that the application depends on. """ - seed_users_from_csv(str(BASE_DIR / "user_data.csv")) - seed_challenge_questions_from_csv(str(BASE_DIR / "challenge_question.csv")) + user_csv = DATA_DIR / "user_data.csv" + challenge_csv = DATA_DIR / "challenge_question.csv" + + for file_path in (user_csv, challenge_csv): + if not file_path.is_file(): + raise FileNotFoundError(f"Seed file not found: {file_path}") + + seed_users_from_csv(str(user_csv)) + seed_challenge_questions_from_csv(str(challenge_csv)) diff --git a/getcloser/backend/app/scripts/user_challenge_status.py b/getcloser/backend/app/scripts/user_challenge_status.py index 65a8d9e..477490c 100644 --- a/getcloser/backend/app/scripts/user_challenge_status.py +++ b/getcloser/backend/app/scripts/user_challenge_status.py @@ -3,7 +3,7 @@ from core.database import SessionLocal, engine, Base from models.challenges import UserChallengeStatus -def seed_users_from_csv(file_path: str): +def seed_challenges_from_csv(file_path: str): Base.metadata.create_all(bind=engine) db: Session = SessionLocal() @@ -12,7 +12,8 @@ def seed_users_from_csv(file_path: str): reader = csv.DictReader(csvfile) for row in reader: user_challenge_status = UserChallengeStatus( - user_id=row["user_id"], + id=int(row["id"]), + user_id=int(row["user_id"]), is_correct=bool(row["is_correct"]), submitted_at=row["submitted_at"], is_redeemed=bool(row["is_redeemed"]), @@ -27,6 +28,3 @@ def seed_users_from_csv(file_path: str): print("โŒ Error while seeding data:", e) finally: db.close() - -if __name__ == "__main__": - seed_users_from_csv("challenge_data.csv") diff --git a/getcloser/backend/app/scripts/user_data_to_question.py b/getcloser/backend/app/scripts/user_data_to_question.py new file mode 100644 index 0000000..191f67f --- /dev/null +++ b/getcloser/backend/app/scripts/user_data_to_question.py @@ -0,0 +1,60 @@ +import csv +import json + +input_file = "input.csv" +output_file = "output.csv" + +rows = [] +id_counter = 1 + +with open(input_file, newline="", encoding="utf-8-sig") as f: + reader = csv.DictReader(f) + + for row in reader: + user_id = row["idx"] + if user_id == "idx": + # Skip duplicated header row in data. + continue + + # category 1: ๊ด€์‹ฌ์‚ฌ ํ‚ค์›Œ๋“œ (JSON ๋ฆฌ์ŠคํŠธ) + raw_interest_keywords = (row.get("interest_keywords") or "").strip() + if raw_interest_keywords: + try: + interests = json.loads(raw_interest_keywords) + except json.JSONDecodeError: + # Fallback for non-JSON input like "a,b,c" + interests = [value.strip() for value in raw_interest_keywords.split(",") if value.strip()] + else: + interests = [] + for interest in interests: + rows.append([ + id_counter, + user_id, + 1, + interest + ]) + id_counter += 1 + + # category 2: ๊ณ„์ ˆ + rows.append([ + id_counter, + user_id, + 2, + row["favorite_season"] + ]) + id_counter += 1 + + # category 3: MBTI + rows.append([ + id_counter, + user_id, + 3, + row["mbti"] + ]) + id_counter += 1 + +# CSV ์ €์žฅ +with open(output_file, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["id", "user_id", "category", "answer"]) + writer.writerows(rows) diff --git a/getcloser/backend/app/services/challenge_service.py b/getcloser/backend/app/services/challenge_service.py index 8a870d5..086a2f0 100644 --- a/getcloser/backend/app/services/challenge_service.py +++ b/getcloser/backend/app/services/challenge_service.py @@ -8,11 +8,11 @@ from models.challenges import UserChallengeStatus -def assign_challenges_logic(my_id: str, members: list, db: Session) -> list: +def assign_challenges_logic(my_id: int, members: list[int], team_id: int, db: Session) -> list: # ํ˜„์žฌ ์‚ฌ์šฉ์ž retry_count ์กฐํšŒ status = db.query(UserChallengeStatus).filter(UserChallengeStatus.user_id == my_id).first() - # โœ… ์—†์œผ๋ฉด ์ƒ์„ฑ + # status ์—†์œผ๋ฉด ์ƒ์„ฑ if not status: status = UserChallengeStatus( user_id=my_id, @@ -26,67 +26,82 @@ def assign_challenges_logic(my_id: str, members: list, db: Session) -> list: # retry_count ๊ฒ€์‚ฌ if status.retry_count >= 2: - return {"message": "retry_count๊ฐ€ 2 ์ด์ƒ์ž…๋‹ˆ๋‹ค. ํŒ€์„ ๋‹ค์‹œ ๊ตฌ์„ฑํ•ด์ฃผ์„ธ์š”."} + team_service.dissolve_team_by_user(db, my_id, team_id) + raise HTTPException(status_code=500, detail="over retry count") - team_questions = db.query(ChallengeQuestion).filter(ChallengeQuestion.user_id.in_(members)).all() - if len(team_questions) < len(members): - raise ValueError("ํŒ€์› ๋ฌธ์ œ๊ฐ€ ์ถฉ๋ถ„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.") - - assigned_list = [] - available_ids = members.copy() - random.shuffle(available_ids) - - for user_id in members: - possible_ids = [uid for uid in available_ids if uid != user_id] - if not possible_ids: - raise ValueError(f"{user_id}์—๊ฒŒ ํ• ๋‹นํ•  ๋ฌธ์ œ ๋ถ€์กฑ") - - assigned_user_id = random.choice(possible_ids) - assigned_question = random.choice([q for q in team_questions if q.user_id == assigned_user_id]) - - available_ids.remove(assigned_user_id) + # ํŒ€์› ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋น„์–ด์žˆ๋Š” ๊ฒฝ์šฐ + if not members: + raise ValueError("members ๋ฆฌ์ŠคํŠธ๊ฐ€ ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํŒ€์› ์ •๋ณด๊ฐ€ ์ „๋‹ฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.") - assigned_list.append(AssignedChallenge( - user_id=user_id, - assigned_challenge_id=assigned_question.id, - from_user_id=assigned_question.user_id, - category=assigned_question.category, - answer=assigned_question.answer - )) - - return assigned_list[0] - - -def submit_challenges_logic(user_id: str, challenge_id: int, submitted_answer: str, db: Session) -> bool: - # 1. ์‚ฌ์šฉ์ž๊ฐ€ ํ‘ผ ๋ฌธ์ œ ์ฐพ๊ธฐ - challenge = db.query(ChallengeQuestion).filter( - ChallengeQuestion.user_id == user_id, - ChallengeQuestion.id == challenge_id - ).first() + # ํŒ€์›๋“ค์ด ๋งŒ๋“  ๋ฌธ์ œ ์กฐํšŒ + team_questions = db.query(ChallengeQuestion).filter(ChallengeQuestion.user_id.in_(members)).all() + if not team_questions: + raise ValueError("๋ฐฐ์ •ํ•  ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.") + + # ์…”ํ”Œ ํ›„ 1๊ฐœ ์„ ํƒ + selected_question = random.choice(team_questions) + + # AssignedChallenge๋กœ ๋ณ€ํ™˜ + assigned_challenge = AssignedChallenge( + assigned_challenge_id=selected_question.id, + user_id=selected_question.user_id, # ๋ฌธ์ œ ์ถœ์ œ์ž + category=selected_question.category, + answer=selected_question.answer, + ) + + # โœ… UserChallengeStatus ์—…๋ฐ์ดํŠธ + user_status = ( + db.query(UserChallengeStatus) + .filter(UserChallengeStatus.user_id == my_id) + .first() + ) + + if not user_status: + raise HTTPException(status_code=404, detail="UserChallengeStatus ์—†์Œ") + + user_status.challenge_id = selected_question.id + user_status.submitted_at = None + user_status.is_correct = False + user_status.is_redeemed = False + + db.add(user_status) + db.commit() + + return assigned_challenge + + +def submit_challenges_logic(my_id: str, challenge_id: int, submitted_answer: str, db: Session) -> bool: + # # 1. ์‚ฌ์šฉ์ž๊ฐ€ ํ‘ผ ๋ฌธ์ œ ์ฐพ๊ธฐ + # challenge = db.query(ChallengeQuestion).filter( + # ChallengeQuestion.user_id == user_id, + # ChallengeQuestion.id == challenge_id + # ).first() - if not challenge: - raise ValueError("ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ํ• ๋‹น๋œ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.") + # if not challenge: + # raise ValueError("ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ํ• ๋‹น๋œ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.") # 2. ์›๋ณธ ๋ฌธ์ œ์—์„œ ์ •๋‹ต ํ™•์ธ question = db.query(ChallengeQuestion).filter( - ChallengeQuestion.id == challenge.id + ChallengeQuestion.id == challenge_id ).first() if not question: raise ValueError("๋ฌธ์ œ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.") # 3. ์ •๋‹ต ํŒ๋ณ„ - is_correct = (submitted_answer.strip().lower() == question.answer.strip().lower()) + answer_keyword = max(question.answer.lower().split(), key=len) + submitted = submitted_answer.strip().lower() + is_correct = answer_keyword in submitted # 4. UserChallengeStatus ์กฐํšŒ ๋˜๋Š” ์ƒ์„ฑ status = db.query(UserChallengeStatus).filter( - UserChallengeStatus.user_id == user_id, + UserChallengeStatus.user_id == my_id, UserChallengeStatus.challenge_id == challenge_id ).first() if not status: status = UserChallengeStatus( - user_id=user_id, + user_id=my_id, challenge_id=challenge_id, retry_count=0 ) diff --git a/getcloser/backend/app/services/team_service.py b/getcloser/backend/app/services/team_service.py index 0d956a1..4effe7f 100644 --- a/getcloser/backend/app/services/team_service.py +++ b/getcloser/backend/app/services/team_service.py @@ -176,24 +176,45 @@ def get_team_status(db: Session, team_id: int, user_id: int): "members_ready": [m.user_id for m in team.members if m.confirmed] } -def dissolve_team_by_user(db: Session, user_id: int): +def dissolve_team_by_user(db: Session, user_id: int, team_id: int): team_entry = ( - db.query(Team) - .join(TeamMember) + db.query(TeamMember) .filter( TeamMember.user_id == user_id, - Team.status == TeamStatus.ACTIVE + TeamMember.team_id == team_id ).first() ) if not team_entry: - raise HTTPException(status_code=400, detail="User is not in an active team") + raise HTTPException(status_code=404, detail="User is not in a team") + + db.delete(team_entry) - team_entry.status = TeamStatus.FAILED + status = ( + db.query(UserChallengeStatus) + .filter(UserChallengeStatus.user_id == user_id) + .first() + ) + if status: + status.retry_count = 0 + status.is_correct = False + status.is_redeemed = False + db.add(status) + db.commit() - - return {"message": f"Team {team_entry.id} dissolved due to quiz failure.", "team_id": team_entry.id} + + remaining = ( + db.query(TeamMember) + .filter(TeamMember.team_id == team_id) + .count() + ) + + if remaining == 0: + team = db.query(Team).get(team_id) + team.status = TeamStatus.CANCELLED + db.commit() + def get_team_info(db: Session, user_id: int): team_member = ( @@ -266,18 +287,20 @@ def get_team_member_challenge( record = ( db.query(UserChallengeStatus) .join(ChallengeQuestion, ChallengeQuestion.id == UserChallengeStatus.challenge_id) - .filter(UserChallengeStatus.user_id == user_id) - .order_by(UserChallengeStatus.created_at.desc()) + .filter( + UserChallengeStatus.user_id == requester_id, + ChallengeQuestion.user_id == user_id + ) + .order_by(UserChallengeStatus.id.desc()) .first() ) if not record: - return {"status": "NO_CHALLENGE"} + raise HTTPException(status_code=404, detail="NO_CHALLENGE") return MemberChallengeResponse( user_id=user_id, - question=record.challenge.question, - user_answer=record.answer, + question=record.challenge.category, correct_answer=record.challenge.answer, is_correct=record.is_correct ) diff --git a/getcloser/backend/app/services/user_service.py b/getcloser/backend/app/services/user_service.py index ed0b59a..ade6959 100644 --- a/getcloser/backend/app/services/user_service.py +++ b/getcloser/backend/app/services/user_service.py @@ -49,9 +49,15 @@ def progress_status( if not team_member: return None, None, ProgressStatus.NONE_TEAM + + if not team_member.confirmed: + return None, None, ProgressStatus.NONE_TEAM team = db.query(Team).get(team_member.team_id) + if not team: + return None, None, ProgressStatus.NONE_TEAM + # 2. ํŒ€ ์ƒํƒœ if team.status == TeamStatus.PENDING: return team.id, None, ProgressStatus.TEAM_WAITING diff --git a/getcloser/docker-compose.yaml b/getcloser/docker-compose.yaml index b554849..581a272 100644 --- a/getcloser/docker-compose.yaml +++ b/getcloser/docker-compose.yaml @@ -48,6 +48,15 @@ services: CORS_ORIGINS: "https://${APP_HOST}" TEAM_SIZE: ${TEAM_SIZE} PENDING_TIMEOUT_MINUTES: ${PENDING_TIMEOUT_MINUTES} + # ์‹œ๋“œ ๋ฐ์ดํ„ฐ ๊ฒฝ๋กœ๋Š” ์ปจํ…Œ์ด๋„ˆ ๋‚ด์—์„œ ๊ณ ์ • (/app/seed-data) + DATA_DIR: /app/seed-data + volumes: + # ํ˜ธ์ŠคํŠธ์— ์žˆ๋Š” ์‹œ๋“œ ๋ฐ์ดํ„ฐ ๊ฒฝ๋กœ๋ฅผ ์ปจํ…Œ์ด๋„ˆ๋กœ ๋ฐ”์ธ๋“œ + # DATA_DIR_HOST๋ฅผ .env์— ์ ˆ๋Œ€๊ฒฝ๋กœ๋กœ ์ง€์ •ํ•˜๋ฉด ์•ฑ ํด๋” ๋ฐ–์˜ ๋ฐ์ดํ„ฐ๋„ ์‚ฌ์šฉ ๊ฐ€๋Šฅ + - type: bind + source: ${DATA_DIR_HOST:-./backend/app/scripts} + target: /app/seed-data + read_only: true depends_on: db: condition: service_healthy @@ -83,6 +92,8 @@ services: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: ${DB_DATABASE} + ports: + - "${DB_PORT:-5432}:5432" volumes: - postgres_data:/var/lib/postgresql/data expose: diff --git a/getcloser/frontend/package-lock.json b/getcloser/frontend/package-lock.json index c776275..9239912 100644 --- a/getcloser/frontend/package-lock.json +++ b/getcloser/frontend/package-lock.json @@ -8,15 +8,18 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@gsap/react": "^2.1.2", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-slot": "^1.2.3", "@tanstack/react-query": "^5.90.2", "boring-avatars": "^2.0.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "gsap": "^3.14.2", "js-cookie": "^3.0.5", "lucide-react": "^0.544.0", "next": "15.5.9", + "ogl": "^1.0.11", "react": "19.2.1", "react-dom": "19.2.1", "tailwind-merge": "^3.3.1", @@ -226,6 +229,16 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@gsap/react": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz", + "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==", + "license": "SEE LICENSE AT https://gsap.com/standard-license", + "peerDependencies": { + "gsap": "^3.12.5", + "react": ">=17" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3613,6 +3626,12 @@ "dev": true, "license": "ISC" }, + "node_modules/gsap": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz", + "integrity": "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4950,6 +4969,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ogl": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ogl/-/ogl-1.0.11.tgz", + "integrity": "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==", + "license": "Unlicense" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6263,96 +6288,6 @@ "optional": true } } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.4.tgz", - "integrity": "sha512-nopqz+Ov6uvorej8ndRX6HlxCYWCO3AHLfKK2TYvxoSB2scETOcfm/HSS3piPqc3A+MUgyHoqE6je4wnkjfrOA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.4.tgz", - "integrity": "sha512-QOTCFq8b09ghfjRJKfb68kU9k2K+2wsC4A67psOiMn849K9ZXgCSRQr0oVHfmKnoqCbEmQWG1f2h1T2vtJJ9mA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.4.tgz", - "integrity": "sha512-eRD5zkts6jS3VfE/J0Kt1VxdFqTnMc3QgO5lFE5GKN3KDI/uUpSyK3CjQHmfEkYR4wCOl0R0XrsjpxfWEA++XA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.4.tgz", - "integrity": "sha512-TOK7iTxmXFc45UrtKqWdZ1shfxuL4tnVAOuuJK4S88rX3oyVV4ZkLjtMT85wQkfBrOOvU55aLty+MV8xmcJR8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.4.tgz", - "integrity": "sha512-PcR2bN7FlM32XM6eumklmyWLLbu2vs+D7nJX8OAIoWy69Kef8mfiN4e8TUv2KohprwifdpFKPzIP1njuCjD0YA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.4.tgz", - "integrity": "sha512-1ur2tSHZj8Px/KMAthmuI9FMp/YFusMMGoRNJaRZMOlSkgvLjzosSdQI0cJAKogdHl3qXUQKL9MGaYvKwA7DXg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/getcloser/frontend/package.json b/getcloser/frontend/package.json index 0b6af77..1c2119f 100644 --- a/getcloser/frontend/package.json +++ b/getcloser/frontend/package.json @@ -9,15 +9,18 @@ "lint": "eslint" }, "dependencies": { + "@gsap/react": "^2.1.2", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-slot": "^1.2.3", "@tanstack/react-query": "^5.90.2", "boring-avatars": "^2.0.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "gsap": "^3.14.2", "js-cookie": "^3.0.5", "lucide-react": "^0.544.0", "next": "15.5.9", + "ogl": "^1.0.11", "react": "19.2.1", "react-dom": "19.2.1", "tailwind-merge": "^3.3.1", diff --git a/getcloser/frontend/public/redeemed-gift.svg b/getcloser/frontend/public/redeemed-gift.svg new file mode 100644 index 0000000..92f2e46 --- /dev/null +++ b/getcloser/frontend/public/redeemed-gift.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + ์ˆ˜๋ น ์™„๋ฃŒ + diff --git a/getcloser/frontend/src/app/Aurora.tsx b/getcloser/frontend/src/app/Aurora.tsx new file mode 100644 index 0000000..2994570 --- /dev/null +++ b/getcloser/frontend/src/app/Aurora.tsx @@ -0,0 +1,195 @@ +import { useEffect, useRef } from 'react'; +import { Renderer, Program, Mesh, Color, Triangle } from 'ogl'; + +const VERT = `#version 300 es +in vec2 position; +void main() { + gl_Position = vec4(position, 0.0, 1.0); +} +`; + +const FRAG = `#version 300 es +precision highp float; + +uniform float uTime; +uniform float uAmplitude; +uniform vec3 uColorStops[3]; +uniform vec2 uResolution; +uniform float uBlend; + +out vec4 fragColor; + +vec3 permute(vec3 x) { + return mod(((x * 34.0) + 1.0) * x, 289.0); +} + +float snoise(vec2 v){ + const vec4 C = vec4( + 0.211324865405187, 0.366025403784439, + -0.577350269189626, 0.024390243902439 + ); + vec2 i = floor(v + dot(v, C.yy)); + vec2 x0 = v - i + dot(i, C.xx); + vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0); + vec4 x12 = x0.xyxy + C.xxzz; + x12.xy -= i1; + i = mod(i, 289.0); + + vec3 p = permute( + permute(i.y + vec3(0.0, i1.y, 1.0)) + + i.x + vec3(0.0, i1.x, 1.0) + ); + + vec3 m = max( + 0.5 - vec3( + dot(x0, x0), + dot(x12.xy, x12.xy), + dot(x12.zw, x12.zw) + ), + 0.0 + ); + m = m * m; + m = m * m; + + vec3 x = 2.0 * fract(p * C.www) - 1.0; + vec3 h = abs(x) - 0.5; + vec3 ox = floor(x + 0.5); + vec3 a0 = x - ox; + m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h); + + vec3 g; + g.x = a0.x * x0.x + h.x * x0.y; + g.yz = a0.yz * x12.xz + h.yz * x12.yw; + return 130.0 * dot(m, g); +} + +struct ColorStop { + vec3 color; + float position; +}; + +#define COLOR_RAMP(colors, factor, finalColor) { \ + int index = 0; \ + for (int i = 0; i < 2; i++) { \ + ColorStop currentColor = colors[i]; \ + bool isInBetween = currentColor.position <= factor; \ + index = int(mix(float(index), float(i), float(isInBetween))); \ + } \ + ColorStop currentColor = colors[index]; \ + ColorStop nextColor = colors[index + 1]; \ + float range = nextColor.position - currentColor.position; \ + float lerpFactor = (factor - currentColor.position) / range; \ + finalColor = mix(currentColor.color, nextColor.color, lerpFactor); \ +} + +void main() { + vec2 uv = gl_FragCoord.xy / uResolution; + + ColorStop colors[3]; + colors[0] = ColorStop(uColorStops[0], 0.0); + colors[1] = ColorStop(uColorStops[1], 0.5); + colors[2] = ColorStop(uColorStops[2], 1.0); + + vec3 rampColor; + COLOR_RAMP(colors, uv.x, rampColor); + + float height = snoise(vec2(uv.x * 2.0 + uTime * 0.1, uTime * 0.25)) * 0.5 * uAmplitude; + height = exp(height); + height = (uv.y * 2.0 - height + 0.2); + float intensity = 0.6 * height; + + float midPoint = 0.20; + float auroraAlpha = smoothstep(midPoint - uBlend * 0.5, midPoint + uBlend * 0.5, intensity); + + vec3 auroraColor = intensity * rampColor; + + fragColor = vec4(auroraColor * auroraAlpha, auroraAlpha); +} +`; + +interface AuroraProps { + colorStops?: string[]; + amplitude?: number; + blend?: number; + time?: number; + speed?: number; +} + +export default function Aurora(props: AuroraProps) { + const { colorStops = ['#5227FF', '#7cff67', '#5227FF'], amplitude = 1.0, blend = 0.5 } = props; + const propsRef = useRef(props); + propsRef.current = props; + + const ctnDom = useRef(null); + + useEffect(() => { + const ctn = ctnDom.current; + if (!ctn) return; + + const renderer = new Renderer({ + alpha: true, + premultipliedAlpha: true, + antialias: true + }); + const gl = renderer.gl; + gl.clearColor(0, 0, 0, 0); + gl.enable(gl.BLEND); + gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + gl.canvas.style.backgroundColor = 'transparent'; + + const program = new Program(gl, { + vertex: VERT, + fragment: FRAG, + uniforms: { + uTime: { value: 0 }, + uAmplitude: { value: amplitude }, + uColorStops: { value: colorStops }, + uResolution: { value: [ctn.offsetWidth, ctn.offsetHeight] }, + uBlend: { value: blend } + } + }); + + const mesh = new Mesh(gl, { geometry: new Triangle(gl), program }); + ctn.appendChild(gl.canvas); + + function resize() { + if (!ctn) return; + const width = ctn.offsetWidth; + const height = ctn.offsetHeight; + renderer.setSize(width, height); + program.uniforms.uResolution.value = [width, height]; + } + window.addEventListener('resize', resize); + + let animateId = 0; + const update = (t: number) => { + animateId = requestAnimationFrame(update); + const { time = t * 0.01, speed = 1.0 } = propsRef.current; + if (program) { + program.uniforms.uTime.value = time * speed * 0.1; + program.uniforms.uAmplitude.value = propsRef.current.amplitude ?? 1.0; + program.uniforms.uBlend.value = propsRef.current.blend ?? blend; + const stops = propsRef.current.colorStops ?? colorStops; + program.uniforms.uColorStops.value = stops.map((hex: string) => { + const c = new Color(hex); + return [c.r, c.g, c.b]; + }); + renderer.render({ scene: mesh }); + } + }; + animateId = requestAnimationFrame(update); + + resize(); + + return () => { + cancelAnimationFrame(animateId); + window.removeEventListener('resize', resize); + if (ctn && gl.canvas.parentNode === ctn) { + ctn.removeChild(gl.canvas); + } + gl.getExtension('WEBGL_lose_context')?.loseContext(); + }; + }, [amplitude, blend, colorStops]); + + return
; +} diff --git a/getcloser/frontend/src/app/layout.tsx b/getcloser/frontend/src/app/layout.tsx index ccb4a8e..3011393 100644 --- a/getcloser/frontend/src/app/layout.tsx +++ b/getcloser/frontend/src/app/layout.tsx @@ -6,6 +6,7 @@ import './globals.css'; import { Providers } from './providers'; import Header from '@/components/Header'; import { useNavigationStore } from '../store/navigationStore'; // Import the navigation store +import Aurora from './Aurora'; const geistSans = Geist({ variable: '--font-geist-sans', @@ -38,17 +39,27 @@ export default function RootLayout({ const hideHeader = currentPage === 'page1'; // Determine if header should be hidden return ( - + {/* eslint-disable-next-line @next/next/no-page-custom-font */} +
+ +
{!hideHeader &&
} {/* Conditionally render the Header */} - {children} +
+ {children} +
); diff --git a/getcloser/frontend/src/app/page.tsx b/getcloser/frontend/src/app/page.tsx index ff0cbfb..63e4767 100644 --- a/getcloser/frontend/src/app/page.tsx +++ b/getcloser/frontend/src/app/page.tsx @@ -25,7 +25,7 @@ export default function Home() { }; return ( -
+
{renderPage()}
); diff --git a/getcloser/frontend/src/app/pages/Page1.tsx b/getcloser/frontend/src/app/pages/Page1.tsx index 9ebc005..59c107d 100644 --- a/getcloser/frontend/src/app/pages/Page1.tsx +++ b/getcloser/frontend/src/app/pages/Page1.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useFormStore } from '../../store/formStore'; +import SplitText from './SplitText'; export default function Page1() { const { email, setEmail, setId, setAccessToken, setTeamId, setChallengeId, setProgressStatus } = useFormStore(); @@ -23,6 +24,10 @@ export default function Page1() { }); if (!authResponse.ok) { + if (authResponse.status === 404) { + alert('๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ ์ž…๋‹ˆ๋‹ค.'); + return; + } throw new Error(`HTTP error! status: ${authResponse.status}`); } @@ -70,7 +75,21 @@ export default function Page1() { return (
-

์นœํ•ด์ง€๊ธธ๋ฐ”๋ผ

+
+ +
Fail

Pseudo Lab

@@ -90,7 +109,6 @@ export default function Page1() { />
-
diff --git a/getcloser/frontend/src/app/pages/Page2.tsx b/getcloser/frontend/src/app/pages/Page2.tsx index 0c0bfb2..70ad601 100644 --- a/getcloser/frontend/src/app/pages/Page2.tsx +++ b/getcloser/frontend/src/app/pages/Page2.tsx @@ -11,7 +11,7 @@ import { authenticatedFetch } from '../../lib/api'; import { useFormStore } from '../../store/formStore'; import { useNavigationStore } from '../../store/navigationStore'; -type View = 'loading' | 'create' | 'waiting'; +type View = 'create' | 'waiting'; type TeamMember = { user_id: number; is_ready: boolean; @@ -45,8 +45,8 @@ const WaitingView = ({ teamMembers, myId, teamId, setView }: { teamMembers: Team
-

ํŒ€์› ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ค‘...

-

๋ชจ๋“  ํŒ€์›์ด ์ค€๋น„๋˜๋ฉด ํ€ด์ฆˆ๊ฐ€ ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค.

+

ํŒ€์› ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ค‘...

+

๋ชจ๋“  ํŒ€์›์ด ์ค€๋น„๋˜๋ฉด ํ€ด์ฆˆ๊ฐ€ ์‹œ์ž‘๋ฉ๋‹ˆ๋‹ค.

{teamMembers.map(member => ( @@ -68,7 +68,7 @@ const WaitingView = ({ teamMembers, myId, teamId, setView }: { teamMembers: Team ))}
- +
@@ -107,7 +107,7 @@ const CreateTeamView = ({
2. ${TEAM_SIZE}๋ช…์ด ํ•จ๊ป˜ ๋ฌธ์ œ ํ’€๊ธฐ์— ๋„์ „ํ•˜์„ธ์š”! (ํŒ! ๋ฌธ์ œ๋Š” ํŒ€์›๋“ค๊ณผ ๊ด€๋ จ๋œ ๋ฌธ์ œ๊ฐ€ ๋‚˜์˜ต๋‹ˆ๋‹ค.)
3. ์„ฑ๊ณต ์‹œ ๋ถ€์Šค ๋ฐฉ๋ฌธํ•ด์ฃผ์„ธ์š”. ๊ธฐ๋…ํ’ˆ์„ ๋“œ๋ฆฝ๋‹ˆ๋‹ค.`)} onConfirm={handleConfirm} onDoNotShowAgain={handleDoNotShowAgain} isOpen={showModal} @@ -132,11 +132,6 @@ const CreateTeamView = ({ ))}
-
); }; @@ -145,7 +140,7 @@ export default function Page2() { const { id: myId, teamId, setTeamId, setMemberIds, progressStatus } = useFormStore(); const { setCurrentPage } = useNavigationStore(); - const [view, setView] = useState('loading'); + const [view, setView] = useState('create'); const [teamMembers, setTeamMembers] = useState([]); const [inputs, setInputs] = useState(() => Array(TEAM_SIZE).fill({ id: '', displayName: '' })); @@ -224,10 +219,28 @@ export default function Page2() { const interval = setInterval(async () => { try { const response = await authenticatedFetch(`/api/v1/teams/${String(teamId)}/status`); - if (!response.ok) throw new Error('Failed to fetch team status'); + + if (!response.ok) { + if (response.status === 404 || response.status === 403) { + console.error('Team not found or user not authorized. Returning to create view.'); + setView('create'); + clearInterval(interval); + return; + } + throw new Error(`Failed to fetch team status: ${response.status}`); + } + const memberStatuses: { team_id: number, status: string, members_ready: number[] } = await response.json(); const readyMemberIds = new Set(memberStatuses.members_ready); + // Per user request, if our ID is not in the list from the server, return to create view. + if (myId && !readyMemberIds.has(Number(myId))) { + console.log('User ID not in members_ready list. Returning to create view.', myId, readyMemberIds); + setView('create'); + clearInterval(interval); + return; + } + setTeamMembers(prevTeamMembers => prevTeamMembers.map(member => ({ ...member, @@ -236,11 +249,13 @@ export default function Page2() { ); } catch (error) { console.error('Error polling team status:', error); + setView('create'); + clearInterval(interval); } }, 2000); return () => clearInterval(interval); - }, [view, teamId]); + }, [view, teamId, myId, setView]); useEffect(() => { if (view === 'waiting' && teamMembers.length > 0 && teamMembers.every(m => m.is_ready)) { @@ -295,14 +310,6 @@ export default function Page2() { } }; - if (view === 'loading') { - return ( -
-
-
- ); - } - if (view === 'waiting') { return ; } diff --git a/getcloser/frontend/src/app/pages/Page3.tsx b/getcloser/frontend/src/app/pages/Page3.tsx index 9a86f38..552f39e 100644 --- a/getcloser/frontend/src/app/pages/Page3.tsx +++ b/getcloser/frontend/src/app/pages/Page3.tsx @@ -1,12 +1,11 @@ 'use client'; import { Button } from '@/components/ui/button'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useFormStore } from '../../store/formStore'; import { authenticatedFetch } from '../../lib/api'; import { useNavigationStore } from '../../store/navigationStore'; +import { questions } from '@/lib/constants'; interface TeamMember { id: number; @@ -17,11 +16,12 @@ interface TeamMember { linkedin_url?: string; } -const questions = [ - { category: '1', keyword: '๊ด€์‹ฌ์‚ฌ', problem: '์‚ฌ์šฉ์ž์˜ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋งž์ถฐ์ฃผ์„ธ์š”. ์˜ˆ: ๊ธฐ์ˆ , ์˜ˆ์ˆ , ํ™˜๊ฒฝ ๋“ฑ' }, - { category: '2', keyword: '์ทจ๋ฏธ', problem: '์‚ฌ์šฉ์ž์˜ ์ทจ๋ฏธ๋ฅผ ๋งž์ถฐ์ฃผ์„ธ์š”. ์˜ˆ: ๋“ฑ์‚ฐ, ๋…์„œ, ์š”๋ฆฌ ๋“ฑ' }, - { category: '3', keyword: 'MBTI', problem: '์‚ฌ์šฉ์ž์˜ MBTI ์œ ํ˜•์„ ๋งž์ถฐ์ฃผ์„ธ์š”. ์˜ˆ: INFP, ESTJ ๋“ฑ' }, -]; +interface QuestionInfo { + category: string; + keyword: string; + problem: string; + options: string[]; +} const getJsonFromResponse = async (response: Response) => { try { @@ -36,13 +36,21 @@ const getJsonFromResponse = async (response: Response) => { }; export default function Page3() { - const { question, answer, challengeId, setAnswer, id, teamId, memberIds, setQuestion, setChallengeId, setIsCorrect, reset } = useFormStore(); // Destructure new state + const { question, challengeId, setAnswer, id, teamId, memberIds, setQuestion, setChallengeId, setProgressStatus, reset } = useFormStore(); // Destructure new state const { setCurrentPage } = useNavigationStore(); + const [currentQuestionInfo, setCurrentQuestionInfo] = useState(null); useEffect(() => { const initializeChallenge = async () => { // If a question is already loaded, do nothing. if (question) { + // Still need to set the question info for rendering options + if (!currentQuestionInfo) { + const loadedQuestionCategory = questions.find(q => question.includes(q.problem)); + if (loadedQuestionCategory) { + setCurrentQuestionInfo(loadedQuestionCategory as QuestionInfo); // Cast to QuestionInfo + } + } console.log('Question already exists, skipping initialization.'); return; } @@ -102,6 +110,7 @@ export default function Page3() { const memberName = findMemberName(userId); setQuestion([memberName, questionInfo.problem].join(' ')); setChallengeId(assigned_challenge_id); + setCurrentQuestionInfo(questionInfo as QuestionInfo); // Cast to QuestionInfo } else { throw new Error(`Could not find question for category: ${category}`); } @@ -120,15 +129,13 @@ export default function Page3() { initializeChallenge(); // Added challengeId to dependency array to react to changes if needed, // though the main trigger is the absence of `question`. - }, [id, teamId, question, memberIds, setQuestion, setChallengeId, reset, setCurrentPage, challengeId]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + }, [id, teamId, question, memberIds, setQuestion, setChallengeId, reset, setCurrentPage, challengeId, currentQuestionInfo]); + const submitAnswer = async (submittedAnswer: string) => { const requestBody = { user_id: id, challenge_id: challengeId, - submitted_answer: answer, + submitted_answer: submittedAnswer, }; try { @@ -147,7 +154,7 @@ export default function Page3() { const responseData = await response.json(); console.log('Challenge submission successful:', responseData); - setIsCorrect(responseData.is_correct); + setProgressStatus(responseData.is_correct ? 'CHALLENGE_SUCCESS' : 'CHALLENGE_FAILED'); setCurrentPage('page4'); } catch (error: unknown) { console.error('Error submitting challenge:', error); @@ -159,32 +166,33 @@ export default function Page3() { } }; + const handleOptionClick = async (option: string) => { + setAnswer(option); // Update the store + await submitAnswer(option); // Submit the answer immediately + }; + return (
-
+

์งˆ๋ฌธ:

{question || '์งˆ๋ฌธ์ด ์ž…๋ ฅ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.'}

-
-
- -