Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions week06/keyword/keyword.md
Original file line number Diff line number Diff line change
@@ -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

192 changes: 192 additions & 0 deletions week06/mission/mission.md
Original file line number Diff line number Diff line change
@@ -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)