https://github.com/narcisource/ghibli-films
스튜디오 지브리 영화의 명장면 감상평 서비스 - GraphQL 연습용 클론
https://github.com/narcisource/ghibli-films
apollo graphql
Last synced: 8 months ago
JSON representation
스튜디오 지브리 영화의 명장면 감상평 서비스 - GraphQL 연습용 클론
- Host: GitHub
- URL: https://github.com/narcisource/ghibli-films
- Owner: NarciSource
- Created: 2025-09-29T14:40:24.000Z (8 months ago)
- Default Branch: main
- Last Pushed: 2025-09-29T15:32:53.000Z (8 months ago)
- Last Synced: 2025-09-29T16:11:50.047Z (8 months ago)
- Topics: apollo, graphql
- Language: SQL
- Homepage: https://narcisource.github.io/Ghibli-Films/
- Size: 519 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# 스튜디오 지브리 영화의 명장면 감상평 서비스
**GraphQL** 학습을 목적으로 제작된 웹 서비스.
REST API의 오버페칭/언더페칭 문제를 해결하기 위해 GraphQL을 도입하고, Apollo + Express를 기반으로 구현.
또한, Elastic Stack(Elasticsearch, Logstash, Kibana) 을 도입하여 MySQL 데이터를 실시간으로 동기화하고,
Elasticsearch 기반의 고성능 검색 기능과 Kibana를 통한 데이터 시각화 및 분석 환경을 제공.
_GraphQL과 타입스크립트로 개발하는 웹 서비스_ (저자: 강화수)에서 제공하는 [🔗예제 프로젝트](https://github.com/hwasurr/graphql-book-fullstack-project)를 바탕으로 함.
## 기술스택
[](https://graphql.org/)
[](https://www.apollographql.com/)
[](https://expressjs.com/ko/)
[](https://www.elastic.co/elastic-stack)
[](https://www.mysql.com/)
[](https://redis.io/)
[](https://typeorm.io/)
[](https://www.elastic.co/kr/elasticsearch)
[](https://www.elastic.co/kr/logstash)
[](https://www.elastic.co/kr/kibana)
[](https://reactjs.org)
[](https://chakra-ui.com/)
[](https://nodejs.org/ko/)
[](https://www.typescriptlang.org/)
[](https://eslint.org/)
[](https://prettier.io/)
[](https://docs.docker.com/compose/)
[](https://www.docker.com/)
[](https://nginx.org/)
## 스크린샷
|  |  |
| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|  |  |
|  |  |
## 다이어그램
### Architecture Diagram

- 백엔드
- **Apollo Server**: Express 플러그인으로 GraphQL query, mutation, resolver 처리
- **Express**: 웹 서버 및 미들웨어 관리
- **비즈니스 로직**: 클라이언트 요청을 받아 MySQL과 Redis에 데이터 저장 및 캐싱
- **MySQL**: 영속적 데이터 저장 (영화, 명장면, 감상평)
- **Redis**: 캐싱 및 성능 최적화
- **Elasticsearch**: 검색 엔진, 영화 데이터에 대한 텍스트 검색 지원
- **Logstash**: 데이터 파이프라인, MySQL에서 Elasticsearch로 동기화
- **Kibana**: Elasticsearch 데이터를 시각화, 검색/로그 분석 및 모니터링 대시보드 제공
- 프론트엔드
- **Apollo Client**: GraphQL 쿼리/뮤테이션 전송, 클라이언트 캐싱, 데이터 페칭
- **React**: UI 렌더링 및 상태 관리
- **Chakra UI**: 웹 UI 구성 및 스타일링
- 데이터 흐름
1. 클라이언트(React)에서 Apollo Client로 GraphQL 요청 전송
2. Apollo Server + Express에서 요청 처리 후 비즈니스 로직 실행
3. MySQL/Elasticsearch/Redis에서 필요한 데이터 조회 또는 저장
1. MySQL에 저장된 데이터는 Logstash 파이프라인을 통해 수집·정제되어 Elasticsearch로 동기화
4. 서버에서 처리된 데이터를 GraphQL Response로 클라이언트에 반환
### GraphQL Schema Diagram
> GraphQL Voyager는 GraphQL 스키마를 시각적으로 탐색하고 구조를 이해할 수 있도록 돕는 정적/인터랙티브 시각화 도구
> 타입과 타입 간 참조를 그래프 형태로 표현
| [](https://narcisource.github.io/Ghibli-Films/) |
| ------------------------------------------------------------------------------------------------------------------------------------------ |
| [GraphQL Voyager 바로가기](https://narcisource.github.io/Ghibli-Films/) |
```mermaid
classDiagram
direction LR
%% 타입 %%
class Cut {
+film : Film
+filmId : Int!
+id : Int!
+isVoted : Boolean!
+src : String!
+votesCount : Int!
}
class CutReview {
+contents : String!
+createdAt : String!
+cutId : Int!
+id : Int!
+isMine : Boolean!
+updatedAt : String!
+user : User!
}
class Director {
+id : Int!
+name : String!
}
class FieldError {
+field : String!
+message : String!
}
class Film {
+description : String!
+director : Director!
+directorId : Int!
+genre : String!
+id : Int!
+posterImg : String!
+releaseDate : String!
+runningTime : Float!
+subtitle : String
+title : String!
}
class LoginResponse {
<>
}
class Mutation:::root {
+createNotification : Notification!
+createOrUpdateReview : CutReview
+deleteReview : Boolean!
+login : LoginResponse!
+logout : Boolean!
+refreshAccessToken : RefreshAccessTokenResponse
+signUp : User!
+uploadProfileImage : Boolean!
+vote : Boolean!
}
class Notification {
+createdAt : String!
+id : Int!
+text : String!
+updatedAt : String!
+userId : Int!
}
class PaginatedFilms {
+cursor : Int
+films : [Film!]!
}
class Query:::root {
+cut : Cut
+cutReviews : [CutReview!]!
+cuts : [Cut!]!
+film : Film
+films : PaginatedFilms!
+me : User
+notifications : [Notification!]!
}
class RefreshAccessTokenResponse {
+accessToken : String!
}
class Subscription:::root {
+newNotification : Notification!
}
class User {
+createdAt : String!
+email : String!
+id : Int!
+profileImage : String
+updatedAt : String!
+username : String!
}
class UserWithToken {
+accessToken : String!
+user : User!
}
%% 관계 %%
Cut --> Film
CutReview --> User
Film --> Director
LoginResponse --> FieldError
LoginResponse --> UserWithToken
UserWithToken --> User
PaginatedFilms --> Film
Query --> Cut
Query --> CutReview
Query --> PaginatedFilms
Query --> Film
Query --> User
Query --> Notification
Mutation --> Notification
Mutation --> LoginResponse
Mutation --> User
Mutation --> CutReview
Mutation --> RefreshAccessTokenResponse
Subscription --> Notification
%% 스타일링 %%
classDef root fill:#EEE
```
### Entity Relationship Diagram
```mermaid
erDiagram
direction LR
FILM {
int id PK
int directorId FK
string title
string subtitle
string genre
int runningTime
string description
string posterImg
string releaseDate
}
CUT {
int id PK
int filmId FK
string src
}
CUT_REVIEW {
int id PK
int cutId FK
int userId FK
string contents
datetime createdAt
datetime updatedAt
}
CUT_VOTE {
int userId PK, FK
int cutId PK, FK
}
USER {
int id PK
string username
string email
string password
string profileImage
datetime createdAt
datetime updatedAt
}
DIRECTOR {
int id PK
string name
}
%% 관계 정의
DIRECTOR ||--o{ FILM : "directs"
FILM ||--o{ CUT : "has"
USER ||--o{ CUT_REVIEW : "writes"
USER ||--o{ CUT_VOTE : "votes"
CUT ||--o{ CUT_REVIEW : "has"
CUT ||--o{ CUT_VOTE : "has"
```
| 테이블 | 설명 | 관계 |
| -------------- | ------------------------------------------------------------------ | ------------------------------- |
| **FILM** | 영화 정보 테이블 (제목, 감독, 장르, 상영시간, 포스터, 개봉년도 등) |
| **CUT** | 영화의 명장면 테이블 (영화ID, 사진URL) | FILM과 1:N 관계 |
| **CUT_REVIEW** | 명장면 감상평 테이블 (명장면ID, 사용자ID, 감상평) | CUT과 USER와 각각 N:1 관계 |
| **CUT_VOTE** | 명장면 투표 저장 테이블 (명장면ID, 사용자ID) | CUT과 USER의 다대다 관계 테이블 |
| **USER** | 사용자 정보 테이블 (유저이름, 비밀번호) |
| **DIRECTOR** | 감독 정보 테이블 |
### Comparison Flowchart
```mermaid
flowchart LR
subgraph REST
rest_client[Client] -->|GET /film/:id| rest_api[REST API]
rest_client -->|GET /cut/:id/reviews| rest_api
rest_client -->|GET /review/:id/user| rest_api
rest_api --> db[(Database)]
end
subgraph GraphQL
graph_client[Client] -->|"POST /graphql {film{cuts{reviews{user}}}}"| graph_api[GraphQL API]
graph_api --> db
end
```
| REST | GraphQL |
| ----------------------------------------- | ---------------------------------------------------------- |
| 여러 엔드포인트 호출 필요 | 단일 엔드포인트(/graphql)에서 요청 처리 |
| 오버페칭/언더페칭 발생 | 클라이언트가 원하는 데이터 구조를 직접 정의 |
| 요청 횟수가 늘어나 네트워크 효율 하락 | 한 번의 요청으로 필요한 데이터만 가져와 응답 사이즈를 감소 |
| 역방향 탐색을 하려면 별도 엔드포인트 필요 | 그래프 모델 기반으로 양방향 탐색의 자유로움 |
GraphQL 쿼리 예시
```js
{
film(id: 1) {
title
cuts {
votesCount
reviews {
contents
user {
username
email
}
}
}
}
}
```
## 폴더 구조
열기
```
Ghibli-Films
├─ docs
│ └─ index.html
├─ data
│ ├─ 01.ddl.sql
│ ├─ 02.directors.sql
│ ├─ 03.films.sql
│ └─ 04.cuts.sql
├─ infra
│ ├─ logstash
│ │ ├─ mysql-connector-j-9.4.0.jar
│ │ └─ sync_rdb_to_es.conf
│ └─ elasticsearch
│ ├─ create_index_templates.sh
│ └─ templates
│ └─ film-template.json
├─ project
│ ├─ server
│ │ ├─ public
│ │ ├─ src
│ │ │ ├─ index.ts
│ │ │ ├─ apollo
│ │ │ │ ├─ IContext.ts
│ │ │ │ ├─ createSchema.ts
│ │ │ │ ├─ createApolloServer.ts
│ │ │ │ ├─ createSubscriptionServer.ts
│ │ │ │ └─ pubSub.ts
│ │ │ ├─ db
│ │ │ │ ├─ db-client.ts
│ │ │ │ └─ es-client.ts
│ │ │ ├─ redis
│ │ │ │ └─ redis-client.ts
│ │ │ ├─ dataloaders
│ │ │ │ ├─ createLoader.ts
│ │ │ │ └─ cutVoteLoader.ts
│ │ │ ├─ middlewares
│ │ │ │ └─ isAuthenticated.ts
│ │ │ ├─ utils
│ │ │ │ └─ jwt-auth.ts
│ │ │ ├─ entities
│ │ │ │ ├─ Cut.ts
│ │ │ │ ├─ CutReview.ts
│ │ │ │ ├─ CutVote.ts
│ │ │ │ ├─ Director.ts
│ │ │ │ ├─ Film.ts
│ │ │ │ ├─ Notification.ts
│ │ │ │ ├─ PaginatedFilm.ts
│ │ │ │ ├─ User.ts
│ │ │ │ ├─ User.Error.ts
│ │ │ │ └─ User.withToken.ts
│ │ │ └─ resolvers
│ │ │ ├─ index.ts
│ │ │ ├─ film
│ │ │ │ ├─ FilmField.ts
│ │ │ │ └─ FilmQuery.ts
│ │ │ ├─ cut
│ │ │ │ ├─ fields
│ │ │ │ │ ├─ Cut.ts
│ │ │ │ │ └─ Review.ts
│ │ │ │ ├─ queries
│ │ │ │ │ ├─ Cut.ts
│ │ │ │ │ └─ Review.ts
│ │ │ │ ├─ mutations
│ │ │ │ │ ├─ CreateOrUpdateReview.ts
│ │ │ │ │ ├─ DeleteReview.ts
│ │ │ │ │ └─ Vote.ts
│ │ │ │ └─ type.ts
│ │ │ ├─ notification
│ │ │ │ ├─ NotificationQuery.ts
│ │ │ │ ├─ NotificationMutation.ts
│ │ │ │ └─ NotificationSubscription.ts
│ │ │ └─ user
│ │ │ ├─ queries
│ │ │ │ └─ Me.ts
│ │ │ ├─ mutations
│ │ │ │ ├─ Login.ts
│ │ │ │ ├─ Logout.ts
│ │ │ │ ├─ SignUp.ts
│ │ │ │ ├─ RefreshAccessToken.ts
│ │ │ │ └─ UploadProfileImage.ts
│ │ │ └─ type.ts
│ │ ├─ .babelrc
│ │ ├─ .env
│ │ ├─ package.json
│ │ └─ tsconfig.json
│ └─ web
│ ├─ public
│ ├─ src
│ │ ├─ index.tsx
│ │ ├─ react-app-env.d.ts
│ │ ├─ reportWebVitals.ts
│ │ ├─ apollo
│ │ │ ├─ createApolloCache.ts
│ │ │ ├─ createApolloClient.ts
│ │ │ ├─ auth.ts
│ │ │ └─ middleware
│ │ │ ├─ authLink.ts
│ │ │ ├─ errorLink.ts
│ │ │ ├─ httpUploadLink.ts
│ │ │ └─ webSocketLink.ts
│ │ ├─ generated
│ │ │ └─ graphql.tsx
│ │ ├─ graphql
│ │ │ ├─ queries
│ │ │ │ ├─ film.graphql
│ │ │ │ ├─ films.graphql
│ │ │ │ ├─ cut.graphql
│ │ │ │ ├─ cuts.graphql
│ │ │ │ ├─ login.graphql
│ │ │ │ ├─ logout.graphql
│ │ │ │ ├─ signup.graphql
│ │ │ │ ├─ me.graphql
│ │ │ │ ├─ refreshAccessToken.graphql
│ │ │ │ └─ notifications.graphql
│ │ │ ├─ mutations
│ │ │ │ ├─ createOrUpdateReview.graphql
│ │ │ │ ├─ deleteReview.graphql
│ │ │ │ ├─ vote.graphql
│ │ │ │ └─ uploadProfileImage.graphql
│ │ │ └─ subscriptions
│ │ │ └─ newNotification.graphql
│ │ ├─ App.tsx
│ │ ├─ components
│ │ │ ├─ auth
│ │ │ │ ├─ LoginForm.layout.tsx
│ │ │ │ ├─ LoginForm.tsx
│ │ │ │ ├─ SignUpForm.layout.tsx
│ │ │ │ └─ SignUpForm.tsx
│ │ │ ├─ ColorModeSwitcher.tsx
│ │ │ ├─ CommonLayout.tsx
│ │ │ ├─ film
│ │ │ │ ├─ FilmCard.tsx
│ │ │ │ ├─ FilmDetail.tsx
│ │ │ │ └─ FilmList.tsx
│ │ │ ├─ film-cut
│ │ │ │ ├─ FilmCutDetail.tsx
│ │ │ │ ├─ FilmCutList.tsx
│ │ │ │ ├─ FilmCutModal.tsx
│ │ │ │ ├─ FilmCutReview.tsx
│ │ │ │ ├─ FilmCutReviewDeleteAlert.tsx
│ │ │ │ └─ FilmCutReviewRegisterModal.tsx
│ │ │ ├─ nav
│ │ │ │ ├─ LogoutItem.tsx
│ │ │ │ ├─ Navbar.tsx
│ │ │ │ ├─ ProfileImageItem.tsx
│ │ │ │ ├─ SearchBar.tsx
│ │ │ │ └─ UserMenu.tsx
│ │ │ └─ notification
│ │ │ ├─ Notification.tsx
│ │ │ ├─ NotificationItem.tsx
│ │ │ └─ useRealtimeAlarm.ts
│ │ └─ pages
│ │ ├─ Main.tsx
│ │ ├─ Film.tsx
│ │ ├─ Login.tsx
│ │ ├─ SignUp.tsx
│ │ └─ Search.tsx
│ ├─ .env
│ ├─ codegen.yml
│ ├─ package.json
│ └─ tsconfig.json
├─ .env
├─ .prettierrc
├─ eslint.config.mjs
├─ package.json
│ └─ package-lock.json
├─ codegen.yml
├─ docker-compose.yml
│ ├─ Dockerfile.server
│ └─ Dockerfile.web
│ └─ nginx.conf
└─ README.md
```
## 실행 방법
```sh
$ docker-compose up -d
```
## 접속 안내
| 환경 | URL |
| ------------------ | -------------------------------- |
| web | |
| server healthcheck | |
| graphql schema | |
| graphql playground | |
| elasticsearch ui | |