diff --git a/week06/keyword/keyword.md b/week06/keyword/keyword.md new file mode 100644 index 0000000..4e193c2 --- /dev/null +++ b/week06/keyword/keyword.md @@ -0,0 +1,93 @@ +- ORM + + SQL을 직접 작성하지 않고도 DB를 조작할 수 있게 해주는 기술 + + DB 독립성, 타입 안정성 + +- Prisma 문서 살펴보기 + - ex. Prisma의 Connection Pool 관리 방법 + + Prisma는 내부적으로 DB 드라이버의 connection pool을 사용함 + + 직접 관리하지 않아도 되지만, `pool_timeout`, `connection_limit`, `timeout` 같은 옵션을 환경 변수(`DATABASE_URL`)로 조정 가능 + + ```bash + DATABASE_URL="postgresql://user:password@localhost:5432/db?connection_limit=5&pool_timeout=10" + ``` + + Prisma Client는 각 요청마다 새로운 연결을 만들지 않고, 연결 풀에서 연결을 재사용하여 효율성 유지 + + - ex. Prisma의 Migration 관리 방법 + + Prisma Migrate 명령어를 통해 스키마 변경을 추적하고 자동으로 SQL 마이그레이션 생성 + + ```bash + npx prisma migrate dev --name add-user-table + ``` + + `prisma/migrations` 폴더에 SQL 파일이 생성되어, DB 스키마 버전을 관리할 수 있음 + +- ORM(Prisma)을 사용하여 좋은 점과 나쁜 점 + + 장점 + + - 마이그레이션 자동 관리 + - 쿼리 빌더보다 단순하고 직관적 + - 타입 안정성 + + 단점 + + - 복잡한 SQL 쿼리는 직접 작성해야 함(집계, 서브쿼리 등) + - Prisma 전용 스키마 파일(schema.prisma)을 유지해야 +- 다양한 ORM 라이브러리 살펴보기 + + + | 언어 | ORM 라이브러리 | 특징 | + | --- | --- | --- | + | JavaScript / TypeScript | **Sequelize** | JS에서 가장 오래된 ORM, 유연하지만 타입 안전성 낮음 | + | | **TypeORM** | 데코레이터 기반, 클래스 중심 ORM | + | | **Prisma** | 스키마 기반, 타입 안전성 최고, 최근 가장 인기 많음 | + | Python | **SQLAlchemy** | 강력한 ORM + 세밀한 쿼리 제어 가능 | + | Java | **Hibernate** | JPA 구현체, 대규모 엔터프라이즈 환경에 적합 | + | Ruby | **ActiveRecord** | Rails의 기본 ORM, 직관적인 문법 | +- 페이지네이션을 사용하는 다른 API 찾아보기 + - ex. https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28 + + 결과를 페이지 단위로 나누어 일부만 반환 + + 기본적으로 한 API 호출당 30개 항목 반환 + + `per_page` 매개변수로 페이지당 최대 100개 항목까지 설정 가능 + + 동작 방식 + + - 응답 헤더에 `link` 필드 포함 + - `link`에는 다음 페이지를 요청할 수 있는 URL이 들어 있음 + - `rel="next"` → 다음 페이지 + - `rel="prev"` → 이전 페이지 + - `rel="first"` → 첫 페이지 + - `rel="last"` → 마지막 페이지 + - ex. https://developers.notion.com/reference/intro#pagination + + 커서 기반 페이지네이션 + + 기본적으로 한 API 호출당 10개 항목 반환 + + `page_size`로 최대 100개까지 설정 가능 + + 요청 흐름 + + - 처음 요청 → `page_size` 지정 + - 응답의 `has_more` 확인 + - `has_more: true` → `next_cursor` 사용해 다음 페이지 요청 + - `has_more: false` → 마지막 페이지 도달 + - https://developer.spotify.com/documentation/web-api/concepts/api-calls?utm_source=chatgpt.com + + offset 기반 페이지네이션 + + offset(시작 위치)과 limit(가져올 항목 수) 파라미터 사용 + + offset: 데이터를 불러올 시작 인덱스 + + limit: 한 번에 가져올 최대 항목 수, 기본값 20, 최대 50 + diff --git a/week06/mission/mission.md b/week06/mission/mission.md new file mode 100644 index 0000000..c9f86dd --- /dev/null +++ b/week06/mission/mission.md @@ -0,0 +1,192 @@ +# 미션 기록 + +`prisma/schema.prisma` + +```jsx +model User { + id Int @id @default(autoincrement()) + email String @unique(map: "email") @db.VarChar(255) + name String @db.VarChar(100) + gender String @db.VarChar(15) + birth DateTime @db.Date + address String @db.VarChar(255) + detailAddress String? @map("detail_address") @db.VarChar(255) + phoneNumber String @map("phone_number") @db.VarChar(15) + + userFavorCategories UserFavorCategory[] + reviews UserStoreReview[] + userMissions UserMission[] + + @@map("user") +} + +model FoodCategory { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + + userFavorCategories UserFavorCategory[] + + @@map("food_category") +} + +model UserFavorCategory { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id]) + userId Int @map("user_id") + foodCategory FoodCategory @relation(fields: [foodCategoryId], references: [id]) + foodCategoryId Int @map("food_category_id") + + @@index([foodCategoryId], map: "f_category_id") + @@index([userId], map: "user_id") + @@map("user_favor_category") +} + +model Region { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + stores Store[] + + @@map("region") +} + +model Store { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + number String? @db.VarChar(50) + thumbnail String? @db.VarChar(255) + work_time String? @db.VarChar(255) + address String? @db.VarChar(255) + + region_id Int + region Region @relation(fields: [region_id], references: [id]) + + reviews UserStoreReview[] + missions Mission[] + @@map("store") +} + +model UserStoreReview { + id Int @id @default(autoincrement()) + store Store @relation(fields: [storeId], references: [id]) + storeId Int @map("store_id") + user User @relation(fields: [userId], references: [id]) + userId Int @map("user_id") + content String @db.Text + + star Int? @db.TinyInt + imageUrl String? @db.VarChar(255) + createdAt DateTime @default(now()) @map("created_at") + + @@map("user_store_review") +} + +model Mission { + id Int @id @default(autoincrement()) + store Store @relation(fields: [storeId], references: [id]) + storeId Int @map("store_id") + status String @default("pending") @db.VarChar(50) + content String @db.Text + deadline DateTime + point Int @default(0) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + userMissions UserMission[] + @@map("mission") +} + +enum UserMissionStatus { + pending // 진행 전 + in_progress // 진행 중 + completed // 완료 +} + +model UserMission { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id]) + userId Int @map("user_id") + mission Mission @relation(fields: [missionId], references: [id]) + missionId Int @map("mission_id") + status UserMissionStatus @default(pending) + createdAt DateTime @default(now()) @map("created_at") + + @@map("user_mission") + @@unique([userId, missionId]) +} +``` + +- id: autoincrement로 변경 +- usermission status: boolean → enum 변경 + + + | enum 값 | 의미 | + | --- | --- | + | `pending` | 진행 전 | + | `in_progress` | 진행 중 | + | `completed` | 완료 | + +**커서 기반 페이지네이션 사용** + +**클라이언트 요청 → 컨트롤러 → 서비스 → 레포지토리 → dto** + + +### 내가 작성한 리뷰 목록 조회 + +`GET /api/v1/users/:userId/reviews` + +1. 클라이언트 요청 +2. 컨트롤러: + - `userId`와 `cursor` 추출 + - 서비스 호출 +3. 서비스: + - 레포지토리 호출 (`getAllUserReviews`) + - DTO 적용 (`responseFromReviews`) +4. Repository: + - Prisma 쿼리 수행 + + ```bash + export const getAllUserReviews = async (userId, cursor = 0) => { + const reviews = await prisma.userStoreReview.findMany({ + select: { + id: true, + content: true, + star: true, + imageUrl: true, + createdAt: true, + storeId: true, + store: { select: { id: true, name: true, thumbnail: true } }, + }, + where: { userId: userId, id: { gt: cursor } }, + orderBy: { id: "asc" }, + take: 5, + }); + + return reviews; + }; + ``` + + - take: 5로 설정, cursor: 마지막으로 가져온 id 기준으로 다음 데이터 가져오기, userId: 특정 사용자의 리뷰만 조회 + - DB에서 리뷰 데이터 반환 +5. 컨트롤러: + - 최종 JSON 응답 전송 + - `pagination.cursor` 포함 → 다음 페이지 요청 가능 + +![스크린샷(231).png](https://github.com/user-attachments/assets/54ac6f12-2ba4-472f-9156-11b00a21449c) + +![스크린샷(232).png](https://github.com/user-attachments/assets/a3fb1de0-0d04-44c5-ae3d-0473a5649856) + +### 특정 가게의 미션 목록 조회 + +`GET /api/v1/stores/:storeId/missions` + +![스크린샷(227).png](https://github.com/user-attachments/assets/927309a7-2711-4720-b198-3b7f4e7f1e74) + +![스크린샷(228).png](https://github.com/user-attachments/assets/f810329d-69c7-4371-9657-c429c2155e2a) + +![스크린샷(229).png](https://github.com/user-attachments/assets/c693b012-101c-4171-b400-a8d1c021be82) + +### 내가 진행 중인 미션 목록 조회 + +`GET /api/v1/users/:userId/missions/inprogress` + +![스크린샷(230).png](https://github.com/user-attachments/assets/0b8770c9-dcd1-4c7c-ba7b-686ffe239ff2)