diff --git a/README.md b/README.md index f2aae05..e92c17e 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,233 @@ -## ๐Ÿ”จ Tech Stack (Node.js Server) +# โœ‰๏ธ ์†์‚ญํŽธ์ง€ (Soksak Letter) -### Languages -- **JavaScript (ES6+)** +> ๋‚˜๋ฅผ ๋งŒ๋‚˜๋Š” ์งˆ๋ฌธ, ํƒ€์ธ๊ณผ ๋‚˜๋ˆ„๋Š” ์ต๋ช… ํŽธ์ง€ +> ๊ณผ๋„ํ•œ ์ž๊ทน์˜ ์‹œ๋Œ€์—, **์„ฑ์ฐฐ๊ณผ ์ง„์ •์„ฑ ์žˆ๋Š” ์—ฐ๊ฒฐ**์„ ๋‹ค์‹œ ๊บผ๋‚ด๋Š” ํŽธ์ง€ ์„œ๋น„์Šค -### Frameworks & Libraries -- **Node.js** -- **Express.js** -- **JWT** -- **Prisma** -- **Swagger** -- **dotenv** +์‚ฌ์šฉ์ž ์„œ๋น„์Šค: https://www.soksak-letter.com/ -### Database -- **MySQL** + -### Tools -- **npm** -- **ESLint** -- **Prettier** -- **Nodemon** +soksak_main -### Version Control -- **Git** -- **GitHub** +
+
---- +## ๐Ÿต Team ์ฒœ์ˆญ์ด -## Convention +### ํŒ€๋ช…: ์ฒœ์ˆญ์ด -### Branch Convention -- **๋ฐฐํฌ์šฉ**: main -- **๊ฐœ๋ฐœ์šฉ**: develop -- **์ž‘์—…์šฉ**: ์ปค๋ฐ‹์œ ํ˜•/#์ด์Šˆ๋ฒˆํ˜ธ-์„ค๋ช… - - ์˜ˆ) feature/#3-์†Œ์…œ๋กœ๊ทธ์ธAPI๊ตฌํ˜„ +๊ฐ€**์ฒœ**๋Œ€(FE: 3๋ช…, BE: 3๋ช…) + **์ˆญ**์‹ค๋Œ€(PM, Designer, FE) => **์ฒœ์ˆญ์ด** + +### R&R + +| ๋ถ„์•ผ | ์ด๋ฆ„ | ํฌ์ง€์…˜ | +| ------ | ------------------------------------------------ | ------------------------------------------------ | +| PM | ์„ธ์ด/์†Œ์•„์—ฐ | ํ”„๋กœ์ ํŠธ ๋งค๋‹ˆ์ง•, ์š”๊ตฌ์‚ฌํ•ญ ์ •์˜, ์ผ์ •/๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ | +| Design | ์šฐ๋‹ˆ/์กฐ์ •์šด | ๋””์ž์ธ ์‹œ์Šคํ…œ, UX/UI, ํ”„๋กœํ† ํƒ€์ž… | +| FE | ์  /๊น€์ง€์›, ์˜ˆ๋””/์ฐจ์˜ˆ๋ฆฐ, ์—์ž‡/์–‘์ˆ˜์ง€, ์œ ๋‹ˆ/๋‚จ์ง€์œค | ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ ๊ตฌํ˜„, API ์—ฐ๋™, ์ƒํƒœ/์บ์‹œ ๊ด€๋ฆฌ | +| BE | ํ‘ธ๋ฏธ/๊น€์„œ์ค€, ๋‘๋ถ€/์กฐ์—ฐ์ค€, ๊ทœ์‹/๋ฐ•์Šน์ฃผ | API/DB ์„ค๊ณ„, ์ธ์ฆ/๋ณด์•ˆ, ๋ฐฐํฌ/์šด์˜ | + +
+ +## ๐Ÿ“ฎ ์„œ๋น„์Šค ๊ฐœ์š” + +### ์„œ๋น„์Šค ์ •์˜ + +**๋‚˜๋ฅผ ๋งŒ๋‚˜๋Š” ์งˆ๋ฌธ, ํƒ€์ธ๊ณผ ๋‚˜๋ˆ„๋Š” ์ต๋ช… ํŽธ์ง€ ์„œ๋น„์Šค** + +- ๋งค์ผ ๋„์ฐฉํ•˜๋Š” โ€œ์˜ค๋Š˜์˜ ์งˆ๋ฌธโ€์— ๋‹ตํ•˜๋ฉฐ ์ƒ๊ฐ์„ ์ •๋ฆฌํ•˜๊ณ  +- ๋งค์นญ๋œ ์ƒ๋Œ€์™€ **์ตœ๋Œ€ 10ํšŒ** ํŽธ์ง€๋ฅผ ์ฃผ๊ณ ๋ฐ›์œผ๋ฉฐ +- **3์ผ ๋’ค ๋‚˜์—๊ฒŒ ์˜ค๋Š” ํŽธ์ง€(D+3)** ๋กœ ๋ฆฌํ…์…˜๊ณผ ํšŒ๋ณต ๊ฒฝํ—˜์„ ๋งŒ๋“ ๋‹ค + +
+ +## ๐ŸŽฏ ๋ฌธ์ œ ์ •์˜ + +### 1) ๋ฌธ์ œ ์ƒํ™ฉ + +ํ˜„๋Œ€์˜ AI/SNS ์‚ฌ์šฉ์ž๋Š” ์ˆํผยทSNSยทAI Slop ๋“ฑ **๋„ํŒŒ๋ฏผ์„ฑ ์ •๋ณด ๊ณผ์ž‰**์— ๋…ธ์ถœ๋˜์–ด, +๊ฐ์ •์ ์œผ๋กœ ์ง€์ณค์„ ๋•Œ ์ฆ‰๊ฐ์ ์ธ ์ž๊ทน์— ์˜์กดํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. +๊ทธ ๊ฒฐ๊ณผ โ€œ์ƒ๊ฐํ•  ์‹œ๊ฐ„โ€์ด ์ค„์–ด **์ž๊ธฐ ์„ฑ์ฐฐ**์ด ์–ด๋ ค์›Œ์ง€๊ณ , ์žฅ๊ธฐ์ ์œผ๋กœ ๋ฒˆ์•„์›ƒ/๊ณ ๋ฆฝ๊ฐ์ด ๋ˆ„์ ๋ฉ๋‹ˆ๋‹ค. + +### 2) ๋ฌธ์ œ ์›์ธ + +- ๊ทผ๋ณธ ์›์ธ: **๋‚ด๋ฉด์˜ ๋ถˆ์•ˆ์ •์„ฑ** + **๊ฑด๊ฐ•ํ•œ ์‚ฌ์œ ๋ฅผ ์œ„ํ•œ ์ฐฝ๊ตฌ ๋ถ€์žฌ** +- ์‰ฌ์šด ํ•ด๊ฒฐ(์ฆ‰๊ฐ ์ž๊ทน, AI ์˜์กด)๋กœ ๊ณ ํ†ต์„ ํšŒํ”ผํ•˜๋Š” ์Šต๊ด€ ๊ฐ•ํ™” +- ๊ธฐ์กด ํ•ด๊ฒฐ์ฑ…(์ผ๊ธฐ/๋ช…์ƒ ์•ฑ)์€ ๋ถ€๋‹ดยท์ง€๋ฃจํ•จ์œผ๋กœ **์ง€์† ์‚ฌ์šฉ ์œ ๋„ ์‹คํŒจ** + +### 3) ๋ฌธ์ œ ์˜ํ–ฅ + +- ์ž๊ธฐ ๊ฐ๊ด€ํ™” ๋Šฅ๋ ฅ ์ €ํ•˜ โ†’ ๊ฐ์ • ์†Œ๋ชจ ์ฆ๊ฐ€ +- ์™ธ๋ถ€ ์ž๊ทน ์˜์กด ๊ฐ•ํ™” โ†’ ์‹ฌ๋ฆฌ์  ํ”ผ๋กœ/๊ณ ๋ฆฝ๊ฐ ์•…ํ™” +- ์žฅ๊ธฐ์ ์œผ๋กœ ์ •์‹ ์  ๊ฑด๊ฐ•์— ๋ถ€์ •์  ์˜ํ–ฅ + +
+ +## ๐Ÿ’ก ๋ชฉ์ (Why) & ๋ฐฐ๊ฒฝ(Background) + +### ์„œ๋น„์Šค ๋ชฉ์ (Why) + +**ํŽธ์ง€์˜ ๊ฐ€์น˜๋ฅผ ํ†ตํ•ด, ์‚ฌ์šฉ์ž์—๊ฒŒ ์„ฑ์ฐฐ๊ณผ ์†Œํ†ต์˜ ๊ธฐํšŒ๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์žฅ๊ธฐ์ ์ธ ์‹ฌ๋ฆฌ์  ์•ˆ์ •๊ฐ์„ ์ค€๋‹ค.** + +### ๋ฐฐ๊ฒฝ(Background) + +| Fact | Insight | +| ----------------------------------------- | -------------------------------------------------------------- | +| ๋””์ง€ํ„ธ ๋””ํ†ก์Šค/๋„ํŒŒ๋ฏผ ๋””ํ†ก์Šค ํŠธ๋ Œ๋“œ ํ™•์‚ฐ | ์‚ฌ์šฉ์ž๋Š” ์ž๊ทน์˜ ์•…์ˆœํ™˜์— ์ง€์ณค๊ณ , **์Šต๊ด€ ๊ต์ •ํ˜• ๊ฒฝํ—˜**์„ ์›ํ•œ๋‹ค | +| SNS ์‚ฌ์šฉ ์ฆ๊ฐ€ โ†” ๋น„๊ต ์‹ฌํ™”๋กœ ์ธํ•œ ๊ณ ๋ฆฝ๊ฐ | ์™ธ๋กœ์›€์€ ์—ฐ๊ฒฐ์˜ ๋ถ€์žฌ๊ฐ€ ์•„๋‹ˆ๋ผ **์—ฐ๊ฒฐ์˜ ํ’ˆ์งˆ ๋ฌธ์ œ**๋‹ค | +| ์‹ฌ๋ฆฌ์ƒ๋‹ด ์‹œ์žฅ ์„ฑ์žฅ, ์‚ฌํšŒ์  ๊ณ ๋ฆฝ ์ด์Šˆ ํ™•๋Œ€ | ํฐ ๋น„์šฉ/์‹œ๊ฐ„ ์—†์ด ์ผ์ƒ ์†์—์„œ **์˜ˆ๋ฐฉ์  ์ž๊ธฐ๊ด€๋ฆฌ** ๋‹ˆ์ฆˆ๊ฐ€ ์žˆ๋‹ค | + +์ฐธ๊ณ (์˜ˆ์‹œ): ์„œ์šธ์‹œ โ€˜์‚ฌํšŒ์  ๊ณ ๋ฆฝโ€™ ์ •์ฑ… ์ž๋ฃŒ: [https://www.seoul.go.kr/policy/view.do?id=1056&lan=KO](https://www.seoul.go.kr/policy/view.do?id=1056&lan=KO) + +
+ +## ๐Ÿ‘€ ์ฃผ์š” ์‚ฌ์šฉ์ž(Target User) + +- ๋Œ€ํ•™์ƒ +- 20๋Œ€ ์ง์žฅ์ธ +- 20๋Œ€ AI/SNS ํ—ค๋น„ ์œ ์ € + +
+ +## ๐Ÿงญ User Journey Map + +> โ€œ์œ ์ž… โ†’ ํƒ์ƒ‰ โ†’ ๋งค์นญ/์†Œํ†ต โ†’ ์ข…๋ฃŒ/ํ”ผ๋“œ๋ฐฑ โ†’ ์žฌ๋ฐฉ๋ฌธ/ํ™•์žฅโ€์˜ ๊ฒฝํ—˜์„ ํ•œ ์žฅ์œผ๋กœ ์„ค๊ณ„ + +| ๋‹จ๊ณ„ | 1. ์œ ์ž… ๋ฐ ์˜จ๋ณด๋”ฉ | 2. ํƒ์ƒ‰ | 3. ๋งค์นญ ๋ฐ ์†Œํ†ต | 4. ์ข…๋ฃŒ ๋ฐ ํ”ผ๋“œ๋ฐฑ | 5. ์žฌ๋ฐฉ๋ฌธ ๋ฐ ํ™•์žฅ | +| ----------- | ------------------------------------------------ | --------------------- | ------------------------- | -------------------------- | ---------------------------------- | +| ์‚ฌ์šฉ์ž ๋ชฉํ‘œ | ๋น ๋ฅด๊ฒŒ ๊ฐ€์ž…ํ•˜๊ณ  ๊ฐ€์น˜ ํ™•์ธ | ์˜ค๋Š˜์˜ ์งˆ๋ฌธ ํ™•์ธ | ํ‹ฐํ‚คํƒ€์นด ๋˜๋Š” ๋Œ€ํ™” | ๊น”๋”ํ•œ ๋งˆ๋ฌด๋ฆฌ/ํ‰๊ฐ€ | ๊ธฐ๋ก ํ™•์ธ/์ถ”๊ฐ€ ์ž‘์„ฑ | +| ์ฃผ์š” ํ–‰๋™ | ์†Œ์…œ ๋กœ๊ทธ์ธ โ†’ ํ”„๋กœํ•„ โ†’ **D+3 ํŽธ์ง€(ํ•„์ˆ˜)** โ†’ ์•Œ๋ฆผ | ์งˆ๋ฌธ ํ™•์ธ โ†’ ๋‹ต๋ณ€ ์ž‘์„ฑ | ๋ฐœ์†กโ†’๋งค์นญโ†’๊ตํ™˜(์ตœ๋Œ€ 10ํšŒ) | ์ข…๋ฃŒ(10ํšŒ/48h) โ†’ ์˜จ๋„/ํƒœ๊ทธ | **3์ผ ์ „ ํŽธ์ง€ ๋„์ฐฉ** โ†’ ์ฃผ๊ฐ„ ๋ฆฌํฌํŠธ | +| ํ„ฐ์น˜ํฌ์ธํŠธ | ๋žœ๋”ฉ/์˜จ๋ณด๋”ฉ/OS ์•Œ๋ฆผ | ํ™ˆ(์˜ค๋Š˜์˜ ์งˆ๋ฌธ ์นด๋“œ) | ์ฑ„ํŒ…๋ฐฉ/์นด์šดํ„ฐ | ๋ฆฌ๋ทฐ ๋ชจ๋‹ฌ/์ข…๋ฃŒ ํŽ˜์ด์ง€ | ๋ณด๊ด€ํ•จ/๋ฆฌํฌํŠธ/DatePicker | +| ๊ธฐํšŒ์š”์ธ | Lock-in(๋ฏธ๋ž˜ ๋ณด์ƒ) | ํฌ์†Œ์„ฑ(24h) | ํ’ˆ์งˆ๊ด€๋ฆฌ(๋‹ต์žฅ๋ฅ /์˜จ๋„) | ๋ฐ์ดํ„ฐ ์„ ์ˆœํ™˜ | ์œ ๋ฃŒํ™” ํ™•์žฅ | + +soksak_ia +soksak_userjourneymap + +
+
+ +## ๐Ÿงฉ ํ•ต์‹ฌ ๊ธฐ๋Šฅ + +- **์˜ค๋Š˜์˜ ์งˆ๋ฌธ(24์‹œ๊ฐ„)** + ๋งค์ผ ์งˆ๋ฌธ ๊ฐฑ์‹  / ์นด์šดํŠธ๋‹ค์šด / ๋‹ต๋ณ€ ์ง„์ž… + +- **์ต๋ช… ๋งค์นญ & ํŽธ์ง€ ๊ตํ™˜(์ตœ๋Œ€ 10ํšŒ, 48์‹œ๊ฐ„)** + ๋Œ€ํ™” ํšŸ์ˆ˜ ์นด์šดํŠธ, ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ + +- **D+3 ๋‚˜์—๊ฒŒ ์“ฐ๋Š” ํŽธ์ง€(์˜จ๋ณด๋”ฉ ํ•„์ˆ˜)** + โ€œ๋ฏธ๋ž˜์˜ ๋‚˜โ€์—๊ฒŒ ๋ณด๋‚ด๋Š” ์˜ˆ์•ฝ ํŽธ์ง€, ์•Œ๋ฆผ์œผ๋กœ ํšŒ์ˆ˜ + +- **์ข…๋ฃŒ ๋ฆฌ๋ทฐ(์˜จ๋„ ์Šฌ๋ผ์ด๋” + ํ›„๊ธฐ ํƒœ๊ทธ)** + ๊ฒฝํ—˜ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ โ†’ ๋‹ค์Œ ๋งค์นญ ํ’ˆ์งˆ ๊ฐœ์„  + +- **์ฃผ๊ฐ„ ๋ฆฌํฌํŠธ** + ์ผ์ฃผ์ผ ๊ธฐ๋ก ์š”์•ฝ/๊ฐ์ • ๋ถ„ํฌ/ํ‚ค์›Œ๋“œ ์ธ์‚ฌ์ดํŠธ ์ œ๊ณต + +- **์นœ๊ตฌ ๊ธฐ๋Šฅ / ํ…Œ๋งˆ ๋ฐฐ๊ฒฝ / ํฐํŠธ ์˜ต์…˜ / ์˜ˆ์•ฝ ํŽธ์ง€ ํ™•์žฅ** + +
+ +## ๐Ÿ” System Architecture + +soksak_infrastructure + +
+
+ +## ๐Ÿ—ƒ๏ธ ERD + +soksak_erd + +
+
+ +## **๐Ÿ’ป Technology** + +### Web + +- ![React](https://img.shields.io/badge/React-61DAFB?style=flat-square&logo=React&logoColor=black)![TypeScript](https://img.shields.io/badge/Typescript-3178C6?style=flat-square&logo=Typescript&logoColor=white) + ![TanStack Query](https://img.shields.io/badge/TanStack%20Query-FF4154?style=flat-square&logo=TanStackQuery&logoColor=white) + ![Zustand](https://img.shields.io/badge/zustand-000000?style=flat-square&logo=zustand&logoColor=white) + ![Tailwind CSS](https://img.shields.io/badge/tailwindcss-06B6D4?style=flat-square&logo=tailwind-css&logoColor=white) + ![Framer Motion](https://img.shields.io/badge/framer_motion-0055FF?style=flat-square&logo=framer&logoColor=white) + +### Server + +- ![IntelliJ IDEA](https://img.shields.io/badge/IntelliJ%20IDEA-000000.svg?style=flat-square&logo=intellij-idea&logoColor=white) + ![Node.js](https://img.shields.io/badge/Node.js-339933?style=flat-square&logo=node.js&logoColor=white) + ![JWT](https://img.shields.io/badge/JWT-black?style=flat-square&logo=JSON%20web%20tokens) +- ![MySQL](https://img.shields.io/badge/MySQL-%2300f.svg?style=flat-square&logo=mysql&logoColor=white) +- ![GitHub Actions](https://img.shields.io/badge/Github%20Actions-%232671E5.svg?style=flat-square&logo=githubactions&logoColor=white) + ![Docker](https://img.shields.io/badge/Docker-%230db7ed.svg?style=flat-square&logo=docker&logoColor=white) + ![Nginx](https://img.shields.io/badge/Nginx-%23009639.svg?style=flat-square&logo=nginx&logoColor=white) + +### Co-working Tool + +- ![GitHub](https://img.shields.io/badge/Github-181717.svg?style=flat-square&logo=github&logoColor=white) + ![Notion](https://img.shields.io/badge/Notion-000000.svg?style=flat-square&logo=notion&logoColor=white) + +
+ +## **๐Ÿพ ๊ธฐ์ˆ  ์Šคํƒ ์„ ์ • ์ด์œ ** + +### **๐Ÿ“˜ย Web ์Šคํƒ ์„ ์ • ์ด์œ ** + +- React + Vite + ์†์‚ญํŽธ์ง€๋Š” ๋ชจ๋ฐ”์ผ 375px ๊ณ ์ • UI ๊ธฐ๋ฐ˜์˜ SPA ์„œ๋น„์Šค๋กœ, SSR์ด ํ•„์ˆ˜์ ์ธ ๊ตฌ์กฐ๋Š” ์•„๋‹™๋‹ˆ๋‹ค. ๋น ๋ฅธ ๊ฐœ๋ฐœ ์†๋„์™€ ๊ฐ€๋ฒผ์šด ๋ฒˆ๋“ค ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•˜๋Š” Vite ๊ธฐ๋ฐ˜ React ๊ตฌ์กฐ๋ฅผ ์ฑ„ํƒํ•˜์—ฌ ๊ฐœ๋ฐœ ํšจ์œจ์„ฑ๊ณผ ๋นŒ๋“œ ์†๋„๋ฅผ ๋™์‹œ์— ํ™•๋ณดํ•˜์˜€์Šต๋‹ˆ๋‹ค. + +- TypeScript + ํŽธ์ง€, ๋งค์นญ, ๋ฆฌํฌํŠธ ๋“ฑ ๋„๋ฉ”์ธ์ด ๋ช…ํ™•ํ•˜๊ณ  API ์‘๋‹ต ๊ตฌ์กฐ๊ฐ€ ์ค‘์š”ํ•œ ์„œ๋น„์Šค ํŠน์„ฑ์ƒ ํƒ€์ž… ์•ˆ์ •์„ฑ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. TypeScript๋ฅผ ํ†ตํ•ด ํ˜‘์—… ์‹œ ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๊ณ , ๋ฆฌํŒฉํ† ๋ง ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ค๋ฅ˜๋ฅผ ์‚ฌ์ „์— ๋ฐฉ์ง€ํ•˜๊ณ ์ž ์„ ์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + +- TanStack Query + ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ์™€ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด TanStack Query๋ฅผ ๋„์ž…ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์บ์‹ฑ, ์ž๋™ refetch, ๋กœ๋”ฉ/์—๋Ÿฌ ์ƒํƒœ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜์—ฌ API ์ค‘์‹ฌ ๊ตฌ์กฐ์—์„œ ์•ˆ์ •์ ์ธ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ์œ ์ง€ํ•˜๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + +- Zustand + ์ „์—ญ ์ƒํƒœ๋ฅผ ์ตœ์†Œํ•œ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๊ฒฝ๋Ÿ‰ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ธ Zustand๋ฅผ ์ฑ„ํƒํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๋ถˆํ•„์š”ํ•œ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์—†์ด ์ง๊ด€์ ์ธ store ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ธ์ฆ ์ƒํƒœ๋‚˜ ๋ชจ๋‹ฌ ๊ด€๋ฆฌ ๋“ฑ UI ์ค‘์‹ฌ ์ „์—ญ ์ƒํƒœ์— ์ ํ•ฉํ•˜๋‹ค๊ณ  ํŒ๋‹จํ•˜์˜€์Šต๋‹ˆ๋‹ค. + +- Tailwind CSS + Figma ๊ธฐ๋ฐ˜ ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๋น ๋ฅด๊ฒŒ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด Tailwind CSS๋ฅผ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค ๊ธฐ๋ฐ˜ ์Šคํƒ€์ผ๋ง์„ ํ†ตํ•ด ์ผ๊ด€๋œ ๋””์ž์ธ์„ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ๊ฐœ๋ฐœ ์†๋„๋ฅผ ๋†’์ผ ์ˆ˜ ์žˆ์–ด ์„ ์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + +- Framer Motion + ํŽธ์ง€ ์ „ํ™˜, ๋ชจ๋‹ฌ ๋“ฑ์žฅ ๋“ฑ ๊ฐ์„ฑ์ ์ธ ์ธํ„ฐ๋ž™์…˜์ด ์ค‘์š”ํ•œ ์„œ๋น„์Šค ํŠน์„ฑ์„ ๊ณ ๋ คํ•˜์—ฌ Framer Motion์„ ๋„์ž…ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ง๊ด€์ ์ธ API์™€ ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ตฌํ˜„์ด ๊ฐ€๋Šฅํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐ•ํ™”ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. + +
+ +### **๐Ÿ“— Server ์Šคํƒ ์„ ์ • ์ด์œ ** + +- Node.js + Express + ์†์‚ญํŽธ์ง€๋Š” REST ๊ธฐ๋ฐ˜ API ์„œ๋ฒ„ ๊ตฌ์กฐ๋กœ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋น ๋ฅธ ๊ฐœ๋ฐœ๊ณผ ์œ ์—ฐํ•œ ๋ผ์šฐํŒ… ๊ตฌ์„ฑ์ด ๊ฐ€๋Šฅํ•œ Node.js์™€ Express๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํ”„๋ก ํŠธ์—”๋“œ์™€์˜ ํ˜‘์—… ํšจ์œจ์„ ๋†’์ด๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + +- MySQL + ํŽธ์ง€, ์œ ์ €, ๋งค์นญ, ๋ฆฌ๋ทฐ ๋“ฑ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•œ ์„œ๋น„์Šค์ด๊ธฐ ๋•Œ๋ฌธ์— ํŠธ๋žœ์žญ์…˜๊ณผ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•˜๋Š” RDBMS์ธ MySQL์„ ์ฑ„ํƒํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ์˜ ์•ˆ์ •์„ฑ๊ณผ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ๋ฐ ์ค‘์ ์„ ๋‘์—ˆ์Šต๋‹ˆ๋‹ค. + +- Redis + ๋งค์นญ ๋Œ€๊ธฐ์—ด ์ฒ˜๋ฆฌ ๋ฐ ์บ์‹ฑ๊ณผ ๊ฐ™์ด ๋น ๋ฅธ ์‘๋‹ต์ด ํ•„์š”ํ•œ ์˜์—ญ์„ ์ตœ์ ํ™”ํ•˜๊ธฐ ์œ„ํ•ด Redis๋ฅผ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„ ๋ถ€ํ•˜๋ฅผ ์ค„์ด๊ณ  ์‘๋‹ต ์†๋„๋ฅผ ๊ฐœ์„ ํ•˜๊ณ ์ž ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + +- JWT + ๋ฌด์ƒํƒœ ์ธ์ฆ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ๋ฐฉ์‹์„ ์ ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„ ํ™•์žฅ์„ฑ๊ณผ ์ธ์ฆ ์ฒ˜๋ฆฌ์˜ ๋‹จ์ˆœํ™”๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ํ•ด๋‹น ๋ฐฉ์‹์„ ์ฑ„ํƒํ•˜์˜€์Šต๋‹ˆ๋‹ค. + +
+ +## ๐Ÿ”– Convention + +### Commit Convention +1. ์ปค๋ฐ‹ ์œ ํ˜• + - Feat + - Fix + - Remove + - Refactor + - Style + - Test + - Chore + - docs + +2. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ +- `Feat: ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ API ๊ตฌํ˜„ (#9)` + +3. ๊ทœ์น™ +- ์ œ๋ชฉ 50์ž ์ด๋‚ด +- ์ œ๋ชฉ ๋ ๋งˆ์นจํ‘œ ๊ธˆ์ง€ +- ๋ณธ๋ฌธ์— ๋ฌด์—‡/์™œ ์ž‘์„ฑ ### Coding Convention - **ํŒŒ์ผ๋ช…**: kebab-case @@ -59,13 +254,11 @@ - PUT / PATCH โ†’ updateProfile() - DELETE โ†’ deleteUser() ---- ### Issue Convention - ์–‘์‹ | **[์ปค๋ฐ‹์œ ํ˜•] ์ด์Šˆ ๋‚ด์šฉ** - ์˜ˆ์‹œ | **[FEAT] ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ API ๊ตฌํ˜„** ---- ### PR Convention 1. PR ํƒ€์ž… @@ -80,51 +273,26 @@ 4. ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ - API ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๋˜๋Š” Swagger ์บก์ฒ˜ ---- -### Commit Convention -1. ์ปค๋ฐ‹ ์œ ํ˜• - - Feat - - Fix - - Remove - - Refactor - - Style - - Test - - Chore - - docs +## ๐Ÿฌ Branch Strategy -2. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ -- `Feat: ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ API ๊ตฌํ˜„ (#9)` +### main -3. ๊ทœ์น™ -- ์ œ๋ชฉ 50์ž ์ด๋‚ด -- ์ œ๋ชฉ ๋ ๋งˆ์นจํ‘œ ๊ธˆ์ง€ -- ๋ณธ๋ฌธ์— ๋ฌด์—‡/์™œ ์ž‘์„ฑ +- ์‹ค์ œ ์„œ๋น„์Šค์— ๋ฐฐํฌ๋˜๋Š” ๋ธŒ๋žœ์น˜ +- ์ง์ ‘ ๊ฐœ๋ฐœํ•˜์ง€ ์•Š์œผ๋ฉฐ, dev ๋ธŒ๋žœ์น˜๋ฅผ ํ†ตํ•ด ๋ณ‘ํ•ฉ + +### dev + +- main์—์„œ ๋ถ„๊ธฐ๋˜๋Š” ๊ฐœ๋ฐœ ํ†ตํ•ฉ ๋ธŒ๋žœ์น˜ +- feature ๋ธŒ๋žœ์น˜์—์„œ ์ž‘์—…ํ•œ ๊ธฐ๋Šฅ์„ ๋ณ‘ํ•ฉ +- ์ถฉ๋ถ„ํ•œ ํ…Œ์ŠคํŠธ ํ›„ main์œผ๋กœ ๋ฐฐํฌ + +### fix/#์ด์Šˆ๋ฒˆํ˜ธ-๊ธฐ๋Šฅ๋ช… + +- ๋ฒ„๊ทธ ์ˆ˜์ • ๋ธŒ๋žœ์น˜ +- dev์—์„œ ๋ถ„๊ธฐํ•˜์—ฌ ์ˆ˜์ • ํ›„ dev๋กœ ๋ณ‘ํ•ฉ +- ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋ธŒ๋žœ์น˜๊ฐ€ dev์— ๋ณ‘ํ•ฉ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ ํ›„ ์ž‘์—… + +
---- - -## SSH Tunneling - -### 1. ํ„ฐ๋„๋ง ์‹คํ–‰ -ํ„ฐ๋ฏธ๋„์„ ์—ด๊ณ  ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•œ ๋’ค prisma ์‚ฌ์šฉ - -``` -ssh -fN -i "public ํ‚ค ์œ„์น˜" - L 3307:(DB๊ฐ€ ์žˆ๋Š” ์„œ๋ธŒ๋„ท private ip):3306 ubuntu@(๋ฐฑ์—”๋“œ ์„œ๋ฒ„ public ip) -``` - -โ”ฃ ๐Ÿ“‚.github
- โ”ฃ ๐Ÿ“‚prisma
- โ”ƒ โ”— ๐Ÿ“œschema.prisma
- โ”ฃ ๐Ÿ“‚src
- โ”ƒ โ”ฃ ๐Ÿ“‚controllers
- โ”ƒ โ”ฃ ๐Ÿ“‚errors
- โ”ƒ โ”ฃ ๐Ÿ“‚repositories
- โ”ƒ โ”ฃ ๐Ÿ“‚services
- โ”ƒ โ”ฃ ๐Ÿ“‚utils
- โ”ƒ โ”ฃ ๐Ÿ“œdb.config.js
- โ”ƒ โ”— ๐Ÿ“œindex.js
- โ”ฃ ๐Ÿ“œ.gitignore
- โ”ฃ ๐Ÿ“œpackage-lock.json
- โ”ฃ ๐Ÿ“œpackage.json
- โ”ฃ ๐Ÿ“œREADME.md
- โ”— ๐Ÿ“œswagger.config.js
+--- \ No newline at end of file diff --git a/src/repositories/letter.repository.js b/src/repositories/letter.repository.js index c695160..628b7b3 100644 --- a/src/repositories/letter.repository.js +++ b/src/repositories/letter.repository.js @@ -91,7 +91,7 @@ export const getLetterDetail = async (id) => { letter: { id: letter.id, title: letter.title, - content: letter.title, + content: letter.content, deliveredAt: letter.deliveredAt, question: letter?.question.content, design: { @@ -467,4 +467,112 @@ export const selectQueuedLetter = async () => { }) return letter; -} \ No newline at end of file +} + +// ์„ธ์…˜๋ณ„ ํŽธ์ง€ ๊ฐœ์ˆ˜ ์กฐํšŒ +export const countLettersBySessionIdAndUserId = async ({ sessionId, userId, letterType }) => { + return await prisma.letter.count({ + where: { + letterType, + sessionId: sessionId, + OR: [ + { receiverUserId: userId }, + { senderUserId: userId } + ] + } + }); +}; + +// ์„ธ์…˜๋ณ„ ์ฝ์ง€ ์•Š์€ ํŽธ์ง€ ๊ฐœ์ˆ˜ ์กฐํšŒ +export const countUnreadLettersBySessionIdAndUserId = async ({ sessionId, userId, letterType }) => { + return await prisma.letter.count({ + where: { + letterType, + sessionId: sessionId, + receiverUserId: userId, + readAt: null, + } + }); +}; + +// ์„ธ์…˜๋ณ„ ๋ฐ›์€ ํŽธ์ง€ ์กฐํšŒ +export const findReceivedLettersBySessionIdAndUserId = async ({ sessionId, userId, letterType }) => { + return await prisma.letter.findMany({ + where: { + receiverUserId: userId, + sessionId: sessionId, + letterType, + }, + orderBy: [{ deliveredAt: "desc" }, { createdAt: "desc" }], + select: { + id: true, + title: true, + deliveredAt: true, + readAt: true, + createdAt: true, + question: { + select: { + content: true + } + }, + design: { + select: { + paper: { + select: { + id: true, + color: true, + } + }, + stamp: { + select: { + id: true, + name: true, + assetUrl: true + } + }, + }, + }, + }, + }); +}; + +// ์„ธ์…˜๋ณ„ ๋ณด๋‚ธ ํŽธ์ง€ ์กฐํšŒ +export const findSentLettersBySessionIdAndUserId = async ({ sessionId, userId, letterType }) => { + return await prisma.letter.findMany({ + where: { + senderUserId: userId, + sessionId: sessionId, + letterType, + }, + orderBy: [{ deliveredAt: "desc" }, { createdAt: "desc" }], + select: { + id: true, + title: true, + deliveredAt: true, + readAt: true, + createdAt: true, + question: { + select: { + content: true + } + }, + design: { + select: { + paper: { + select: { + id: true, + color: true, + } + }, + stamp: { + select: { + id: true, + name: true, + assetUrl: true + } + }, + }, + }, + }, + }); +}; \ No newline at end of file diff --git a/src/repositories/session.repository.js b/src/repositories/session.repository.js index 86af207..01ef6f5 100644 --- a/src/repositories/session.repository.js +++ b/src/repositories/session.repository.js @@ -202,3 +202,24 @@ export const countMatchingSessionByUserId = async (userId) => { return count; }; + +// ์„ธ์…˜ ID์™€ ์‚ฌ์šฉ์ž ID๋กœ ์„ธ์…˜ ์กฐํšŒ (์ฐธ๊ฐ€์ž ํฌํ•จ) +export const findMatchingSessionBySessionIdAndUserId = async ({ sessionId, userId }) => { + return await prisma.matchingSession.findFirst({ + where: { + id: sessionId, + participants: { + some: { + userId: userId + } + } + }, + include: { + participants: { + select: { + userId: true + } + } + } + }); +}; \ No newline at end of file diff --git a/src/repositories/user.repository.js b/src/repositories/user.repository.js index 3b6f051..1583579 100644 --- a/src/repositories/user.repository.js +++ b/src/repositories/user.repository.js @@ -591,6 +591,11 @@ export const findSelfLetters = async ({ userId, letterType }) => { createdAt: true, deliveredAt: true, questionId: true, + question: { + select: { + content: true + } + }, design: { select: { paperId: true, diff --git a/src/services/mailbox.service.js b/src/services/mailbox.service.js index 1e96796..0f17cf4 100644 --- a/src/services/mailbox.service.js +++ b/src/services/mailbox.service.js @@ -6,8 +6,15 @@ import { import { LETTER_TYPE_ANON, LETTER_TYPE_SELF, makePreview } from "../utils/user.util.js"; import { findFriendById } from "../repositories/friend.repository.js"; import { NotFriendError } from "../errors/friend.error.js"; -import { getFriendLetters, getMyLettersWithFriend } from "../repositories/letter.repository.js"; -import { prisma } from "../configs/db.config.js"; +import { + getFriendLetters, + getMyLettersWithFriend, + countLettersBySessionIdAndUserId, + countUnreadLettersBySessionIdAndUserId, + findReceivedLettersBySessionIdAndUserId, + findSentLettersBySessionIdAndUserId, +} from "../repositories/letter.repository.js"; +import { findMatchingSessionBySessionIdAndUserId } from "../repositories/session.repository.js"; import { MailboxInvalidSessionIdError } from "../errors/mailbox.error.js"; // ------------------------------ @@ -62,15 +69,10 @@ export const getAnonymousThreads = async (userId) => { // ๊ฐ ์„ธ์…˜๋ณ„ ํŽธ์ง€ ๊ฐœ์ˆ˜ ์กฐํšŒ (sessionId ๊ธฐ์ค€, ๊ฐœ๋ณ„ ์กฐํšŒ) const letterCounts = await Promise.all( sessionIds.map(async (sessionId) => { - const count = await prisma.letter.count({ - where: { - letterType: LETTER_TYPE_ANON, - sessionId: sessionId, - OR: [ - { receiverUserId: userId }, - { senderUserId: userId } - ] - } + const count = await countLettersBySessionIdAndUserId({ + sessionId, + userId, + letterType: LETTER_TYPE_ANON, }); return [sessionId, count]; }) @@ -80,13 +82,10 @@ export const getAnonymousThreads = async (userId) => { // ๊ฐ ์„ธ์…˜๋ณ„ ์ฝ์ง€ ์•Š์€ ํŽธ์ง€ ์ฒดํฌ (๋ฐ›์€ ํŽธ์ง€๋งŒ ์ฒดํฌ) const unreadChecks = await Promise.all( sessionIds.map(async (sessionId) => { - const unreadCount = await prisma.letter.count({ - where: { - letterType: LETTER_TYPE_ANON, - sessionId: sessionId, - receiverUserId: userId, - readAt: null, // ์ฝ์ง€ ์•Š์€ ํŽธ์ง€ - } + const unreadCount = await countUnreadLettersBySessionIdAndUserId({ + sessionId, + userId, + letterType: LETTER_TYPE_ANON, }); return [sessionId, unreadCount > 0]; // ํ•˜๋‚˜๋ผ๋„ ์žˆ์œผ๋ฉด true }) @@ -129,22 +128,9 @@ export const getAnonymousThreadLetters = async (userId, sessionIdRaw) => { const sessionId = Number(sessionIdRaw); // ์„ธ์…˜ ์ฐธ๊ฐ€์ž ์กฐํšŒํ•˜์—ฌ ๊ถŒํ•œ ํ™•์ธ - const session = await prisma.matchingSession.findFirst({ - where: { - id: sessionId, - participants: { - some: { - userId: userId // ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ฐธ๊ฐ€์ž์ธ ์„ธ์…˜๋งŒ - } - } - }, - include: { - participants: { - select: { - userId: true - } - } - } + const session = await findMatchingSessionBySessionIdAndUserId({ + sessionId, + userId, }); if (!session) { @@ -152,81 +138,17 @@ export const getAnonymousThreadLetters = async (userId, sessionIdRaw) => { } // ๋ฐ›์€ ํŽธ์ง€ ์กฐํšŒ (sessionId๋กœ ํ•„ํ„ฐ๋ง) - const receivedLetters = await prisma.letter.findMany({ - where: { - receiverUserId: userId, - sessionId: sessionId, - letterType: LETTER_TYPE_ANON, - }, - orderBy: [{ deliveredAt: "desc" }, { createdAt: "desc" }], - select: { - id: true, - title: true, - deliveredAt: true, - readAt: true, - createdAt: true, - question: { - select: { - content: true - } - }, - design: { - select: { - paper: { - select: { - id: true, - color: true, - } - }, - stamp: { - select: { - id: true, - name: true, - assetUrl: true - } - }, - }, - }, - }, + const receivedLetters = await findReceivedLettersBySessionIdAndUserId({ + sessionId, + userId, + letterType: LETTER_TYPE_ANON, }); // ๋ณด๋‚ธ ํŽธ์ง€ ์กฐํšŒ (sessionId๋กœ ํ•„ํ„ฐ๋ง) - const sentLetters = await prisma.letter.findMany({ - where: { - senderUserId: userId, - sessionId: sessionId, - letterType: LETTER_TYPE_ANON, - }, - orderBy: [{ deliveredAt: "desc" }, { createdAt: "desc" }], - select: { - id: true, - title: true, - deliveredAt: true, - readAt: true, - createdAt: true, - question: { - select: { - content: true - } - }, - design: { - select: { - paper: { - select: { - id: true, - color: true, - } - }, - stamp: { - select: { - id: true, - name: true, - assetUrl: true - } - }, - }, - }, - }, + const sentLetters = await findSentLettersBySessionIdAndUserId({ + sessionId, + userId, + letterType: LETTER_TYPE_ANON, }); // firstQuestion: ๋ฐ›์€ ํŽธ์ง€์˜ ์ฒซ ๋ฒˆ์งธ ์งˆ๋ฌธ ์šฐ์„ , ์—†์œผ๋ฉด ๋ณด๋‚ธ ํŽธ์ง€์˜ ์ฒซ ๋ฒˆ์งธ ์งˆ๋ฌธ @@ -296,7 +218,7 @@ export const getSelfMailbox = async (userId) => { id: l.id, title: l.title, createdAt: l.createdAt ?? null, - questionId: l.questionId ?? null, + questionTitle: l.question?.content ?? null, paperId: l.design?.paperId ?? null, stampId: l.design?.stampId ?? null, stampUrl: l.design?.stamp?.assetUrl ?? null, diff --git a/src/swagger/mailbox.swagger.js b/src/swagger/mailbox.swagger.js index 44a1ee3..3b1ffa5 100644 --- a/src/swagger/mailbox.swagger.js +++ b/src/swagger/mailbox.swagger.js @@ -522,9 +522,10 @@ * type: string * format: date-time * nullable: true - * questionId: - * type: integer + * questionTitle: + * type: string * nullable: true + * description: ์งˆ๋ฌธ ๋‚ด์šฉ * paperId: * type: integer * nullable: true