diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..3e254b032 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +**/.DS_Store +.git +.github +node_modules +mission2 +mission3 +mission4 +mission5 +mission8 +mission9-10-11/node_modules +mission9-10-11/dist +mission9-10-11/coverage +mission9-10-11/.env* +mission9-10-11/.git diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4ed8a24dc..ec85f6f1a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,97 +1,21 @@ -# 요구사항 -### 클래스 구현하기 +## 요구사항 -- [x] class 키워드를 이용해서 Product 클래스와 ElectronicProduct 클래스를 만들어 주세요. - - [x] Product 클래스는 name(상품명) description(상품 설명), price(판매 가격), tags(해시태그 배열), images(이미지 배열), - favoriteCount (찜하기 수)프로퍼티를 가집니다. - - [x] Product 클래스는 favorite 메소드를 가집니다. favorite 메소드가 호출될 경우 찜하기 수가 1 증가합니다. - - [x] ElectronicProduct 클래스는 Product를 상속하며, 추가로 manufacturer(제조사) 프로퍼티를 가집니다. -- [x] class 키워드를 이용해서 Article 클래스를 만들어 주세요. - - [x] Article 클래스는 title(제목), content(내용), writer(작성자), likeCount(좋아요 수) 프로퍼티를 가집니다. - - [x] Article 클래스는 like 메소드를 가집니다. like 메소드가 호출될 경우 좋아요 수가 1 증가합니다. - - [x] 각 클래스 마다 constructor를 작성해 주세요. - - [x] 추상화/캡슐화/상속/다형성을 고려하여 코드를 작성해 주세요. +### 기본 +- [x] 기본 항목 1 +- [ ] 기본 항목 2 -### Article 요청 함수 구현하기 - -- [x] https://panda-market-api-crud.vercel.app/docs 의 Article API를 이용하여 아래 함수들을 구현해 주세요. - - [x] getArticleList() : GET 메소드를 사용해 주세요. - - [x] page, pageSize, keyword 쿼리 파라미터를 이용해 주세요. - - [x] getArticle() : GET 메소드를 사용해 주세요. - - [x] createArticle() : POST 메소드를 사용해 주세요. - - [x] request body에 title, content, image 를 포함해 주세요. - - [x] patchArticle() : PATCH 메소드를 사용해 주세요. - - [x] deleteArticle() : DELETE 메소드를 사용해 주세요. -- [x] fetch 혹은 axios를 이용해 주세요. - - [x] 응답의 상태 코드가 2XX가 아닐 경우, 에러 메시지를 콘솔에 출력해 주세요. -- [x] .then() 메소드를 이용하여 비동기 처리를 해주세요. -- [x] .catch() 를 이용하여 오류 처리를 해주세요. - -### Product 요청 함수 구현하기 - -- [x] https://panda-market-api-crud.vercel.app/docs 의 Product API를 이용하여 아래 함수들을 구현해 주세요. - - [x] getProductList() : GET 메소드를 사용해 주세요. - - [x] page, pageSize, keyword 쿼리 파라미터를 이용해 주세요. - - [x] getProduct() : GET 메소드를 사용해 주세요. - - [x] createProduct() : POST 메소드를 사용해 주세요. - - [x] request body에 name, description, price, tags, images 를 포함해 주세요. - - [x] patchProduct() : PATCH 메소드를 사용해 주세요. - - [x] deleteProduct() : DELETE 메소드를 사용해 주세요. -- [x] async/await 을 이용하여 비동기 처리를 해주세요. -- [x] try/catch 를 이용하여 오류 처리를 해주세요. -- [x] getProductList()를 통해서 받아온 상품 리스트를 각각 인스턴스로 만들어 products 배열에 저장해 주세요. - - [x] 해시태그에 "전자제품"이 포함되어 있는 상품들은 Product 클래스 대신 ElectronicProduct 클래스를 사용해 인스턴스를 생성해 주세요. - - [x] 나머지 상품들은 모두 Product 클래스를 사용해 인스턴스를 생성해 주세요. -- [x] 구현한 함수들을 아래와 같이 파일을 분리해 주세요. - - [x] export를 활용해 주세요. - - [x] ProductService.js 파일 Product API 관련 함수들을 작성해 주세요. - - [x] ArticleService.js 파일에 Article API 관련 함수들을 작성해 주세요. -- [x] 이외의 코드들은 모두 main.js 파일에 작성해 주세요. - - [x] import를 활용해 주세요. - - [x] 각 함수를 실행하는 코드를 작성하고, 제대로 동작하는지 확인해 주세요. - -### (심화)Article 요청 함수 구현하기 - -- [x] Article 클래스에 createdAt(생성일자) 프로퍼티를 만들어 주세요. - - [x] 새로운 객체가 생성되어 constructor가 호출될 시 createdAt에 현재 시간을 저장합니다 - ---- - -## Git과 Github 활용하기 - -- [x] README.md 파일을 작성해 주세요 - - [x] 마크다운 언어를 숙지하여 작성해 주세요. - - [x] 내용은 자유롭게 작성해 주세요. -- [x] 본인 브랜치(ex)홍길동)에 스프린트 미션을 업로드 해 주세요. -- [x] 적절한 커밋 메시지를 남겨 주세요. -- [x] N-Sprint-Mission 레포지토리를 fork 합니다. (e.g. 2기면 2-Sprint-Mission) -- [x] GitHub에 PR(Pull Request)을 생성해 upstream의 본인 브랜치(ex)홍길동)에 미션을 제출합니다. -- [x] PR 커멘트에 아래 내용들을 포함해 주세요. - - [x] 스프린트 미션 요구사항 체크리스트 - - [x] 체크리스트(- [ ]) 를 만듭니다. - - [x] 완료한 만큼 체크 표시 (- [x]) 를 해 주세요. - ---- +### 심화 +- [ ] 심화 항목 1 +- [ ] 심화 항목 2 ## 주요 변경사항 - -- Article 요청 함수는 axios를 활용해 만들었습니다. -- 코드를 작성하면서 잘 모르겠는 부분이나, 모범 답안을 참고하여 작성한 코드는 주석 처리하여 표기했습니다. -- 계속 코드를 수정하며 완료된 부분, 정돈된 부분들은 주석을 삭제하겠습니다. - ---- +- +- ## 스크린샷 - -### ![alt text](<스크린샷 2025-10-17 오후 1.34.51.png>) - -### ![alt text](<스크린샷 2025-10-17 오후 1.35.25.png>) - -### ![alt text](<스크린샷 2025-10-17 오후 1.36.25-1.png>) - ---- +![image](이미지url) ## 멘토에게 - -- 코드 작성하면서 잘 몰랐던 부분들이나 이해가 필요한 부분 등등, 특이사항은 주석처리하여 작성했습니다. 감사합니다. +- 셀프 코드 리뷰를 통해 질문 이어가겠습니다. +- diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..0b047be30 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,40 @@ +name: Deploy to EC2 + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: self-hosted + + env: + NODE_ENV: production + DATABASE_URL: "${{ secrets.DATABASE_URL }}" + JWT_ACCESS_TOKEN_SECRET: "${{ secrets.JWT_ACCESS_TOKEN_SECRET }}" + JWT_REFRESH_TOKEN_SECRET: "${{ secrets.JWT_REFRESH_TOKEN_SECRET }}" + NODE_OPTIONS: "--max-old-space-size=512" + + defaults: + run: + working-directory: mission9-10-11 + + steps: + - uses: actions/checkout@v4 + + - name: Install deps + run: npm ci --include=dev --no-audit --no-fund + + - name: Build + run: npm run build + + - name: Migrate + run: npm run prisma:migrate:deploy + + - name: Restart app + run: | + pm2 reload panda-market || pm2 start dist/src/main.js --name panda-market + pm2 save + + - name: Done + run: echo "Deployment successful!" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..2686cfd0a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,57 @@ +name: Test + +on: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + # 모든 run 단계의 기본 작업 디렉토리를 mission9-10-11로 지정 + defaults: + run: + working-directory: mission9-10-11 + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: mission_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres -d mission_test" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/mission_test + NODE_ENV: test + JWT_ACCESS_TOKEN_SECRET: test-secret + JWT_REFRESH_TOKEN_SECRET: test-refresh-secret + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + check-latest: true + cache: npm + cache-dependency-path: mission9-10-11/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Create CI .env.test + # CI 전용 .env.test 생성 (로컬/운영 비밀값과 분리) + run: | + echo 'DATABASE_URL="postgresql://postgres:postgres@localhost:5432/mission_test"' > .env.test + + - name: Type Check + run: npm run typecheck + + - name: Run Tests + run: npm run test diff --git "a/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.34.51.png" "b/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.34.51.png" deleted file mode 100644 index dbd19bdaf..000000000 Binary files "a/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.34.51.png" and /dev/null differ diff --git "a/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.35.25.png" "b/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.35.25.png" deleted file mode 100644 index 071c53457..000000000 Binary files "a/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.35.25.png" and /dev/null differ diff --git "a/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.36.25-1.png" "b/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.36.25-1.png" deleted file mode 100644 index 7658aff6a..000000000 Binary files "a/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.36.25-1.png" and /dev/null differ diff --git "a/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.36.25.png" "b/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.36.25.png" deleted file mode 100644 index 7658aff6a..000000000 Binary files "a/.github/\354\212\244\355\201\254\353\246\260\354\203\267 2025-10-17 \354\230\244\355\233\204 1.36.25.png" and /dev/null differ diff --git a/.gitignore b/.gitignore index 035af03d8..592efc023 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ public/* .env.* !.env.example !.env.test.example +!.env.production.example \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..a7bcf0760 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# 의존성 설치 +FROM node:22-alpine AS deps +WORKDIR /app +COPY mission9-10-11/package*.json ./ +RUN npm ci + +# 소스 복사 후 빌드 +FROM node:22-alpine AS build +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY mission9-10-11/ ./ +RUN npm run prisma:generate +RUN npm run build + +# runtime 실행에 필요한 것만 복사 +FROM node:22-alpine AS runtime +WORKDIR /app +ENV NODE_ENV=production +COPY --from=build /app/package*.json ./ +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY --from=build /app/prisma ./prisma +COPY --from=build /app/public ./public + +# 컨테이너에서 3000 포트 사용 +EXPOSE 3000 + +CMD ["node", "dist/src/main.js"] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 000000000..b0d280ffc --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,33 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + NODE_ENV: development + PORT: 3000 + UPLOAD_PROVIDER: local + DATABASE_URL: postgresql://postgres:postgres@db:5432/mission_db?schema=public + JWT_ACCESS_TOKEN_SECRET: ${JWT_ACCESS_TOKEN_SECRET} + JWT_REFRESH_TOKEN_SECRET: ${JWT_REFRESH_TOKEN_SECRET} + depends_on: + - db + volumes: + - uploads:/app/public + + db: + image: postgres:15 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: mission_db + ports: + - "5434:5432" + volumes: + - dbdata:/var/lib/postgresql/data + +volumes: + uploads: + dbdata: diff --git a/mission2/.gitignore b/mission2/.gitignore deleted file mode 100644 index 526162b76..000000000 --- a/mission2/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ - -### Mac Os ### -.DS_Store \ No newline at end of file diff --git a/mission2/.prettierrc b/mission2/.prettierrc deleted file mode 100644 index 92f97e756..000000000 --- a/mission2/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "semi": true, - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5" -} diff --git a/mission2/Article.js b/mission2/Article.js deleted file mode 100644 index e9fcb3c1b..000000000 --- a/mission2/Article.js +++ /dev/null @@ -1,23 +0,0 @@ -export class Article { - //좋아요 수, 생성일자 캡슐화 - #likeCount = 0; - #createdAt; - - constructor({ title, content, writer, likeCount = 0 }) { - this.title = title; //제목 - this.content = content; //내용 - this.writer = writer; //작성자 - this.#likeCount = likeCount; //좋아요 수 - this.#createdAt = new Date(); //생성 일자 - } - //좋아요 수 증가 - like() { - this.#likeCount += 1; - } - getlikeCount() { - return this.#likeCount; - } - getcreatedAt() { - return this.#createdAt; - } -} diff --git a/mission2/ArticleService.js b/mission2/ArticleService.js deleted file mode 100644 index af2427331..000000000 --- a/mission2/ArticleService.js +++ /dev/null @@ -1,92 +0,0 @@ -import axios from 'axios'; -//미션 요구 사항에 fetch나 axios를 이용해 작성하라고 나와있어서 article service는 axios를 product service는 fetch를 이용했습니다. -const AtcService = { - getArticleList, - getArticle, - createArticle, - patchArticle, - deleteArticle, -}; -export default atcService; //article을 줄여서 atc로 표기해봤는데 생각해보니 나만 아는 표기법인 것 같아 '좀 더 직관적이게 작성해야 했나?' 생각됩니다. - -const instance = axios.create({ - baseURL: 'https://panda-market-api-crud.vercel.app', - timeout: 2000, -}); - -function getArticleList(queryParams = {}) { - if (queryParams == null) - return Promise.reject(new Error('쿼리 값을 입력하세요.')); //코드를 다시 확인하다보니, 에러 메세지에 쿼리 값을 입력하라는 게 적절한 표현인지 잘 모르겠습니다. - return instance - .get('/articles', { params: queryParams }) - .then((res) => res.data) - .catch((err) => { - console.error( - `getArticleList 실패, 상태 코드:`, //서버 응답이 있는지 없는지, 있는데 에러가 났다면 어떤 상태인지 알기 위해 상태코드를 따로 출력하도록 했습니다. - err.response?.status ?? err.message //옵셔널 체이닝과, 널 병합 연산자는 ai로 학습하면서 알게됐는데 ES2020에 추가된 문법이라고 하는데, 실무에서 많이 쓰이는지 궁금합니다. - ); - throw err; - }); -} - -/*저는 주로 ai와 lms강의 통해 학습했는데, id가 비어있는 상태일 때만 에러메세지가 나오도록 코드를 구성했습니다. ai는 id가 비어있을 때에만 리젝트하기에 비교적 약한 검증이다. -문자열인지 아닌지 등등 비교적 강한 검증?이 필요하다고 했습니다. 만약 실무라면 검증 단계에서 지금보다는 좀 더 빡빡한 검증을 필요로 하나요? 아직 경험이 없어서 그런지..에러가 날만한 -상황들을 생각하지 못했기에 생각이 '아 문자열이 아닌 걸 입력할 수 도 있겠구나'라는 단계까지 가지 못했습니다. 이런 에러가 날 변수들에 대해서는 미연에 방지할 수 있는 것들은 -최대한 방지하고 후에 디버깅하며 코드를 고쳐나가는 것이 문제 해결 능력을 기르는 데 최선인가요?, 아니면 당연한 것인데 아직 익숙치 않아서 그런 것인가요ㅠ*/ -function getArticle(id) { - if (id == null) return Promise.reject(new Error('조회할 id를 입력하세요.')); - return instance - .get(`/articles/${id}`) - .then((res) => res.data) - .catch((err) => { - console.error( - `getArticle(id) 실패, 상태 코드:`, - err.response?.status ?? err.message - ); - throw err; - }); -} - -function createArticle(articleData) { - return instance - .post('/articles', articleData) - .then((res) => res.data) - .catch((err) => { - console.log( - `createArticle 실패, 상태 코드:`, - err.response?.status ?? err.message - ); - throw err; - }); -} - -function patchArticle(id, patchData) { - if (id == null) return Promise.reject(new Error('수정할 id를 입력하세요.')); - return instance - .patch(`/articles/${id}`, patchData) - .then((res) => res.data) - .catch((err) => { - console.log( - `patchArticle 실패, 상태 코드:`, - err.response?.status ?? err.message - ); - throw err; - }); -} - -function deleteArticle(id) { - if (id == null) return Promise.reject(new Error('삭제할 id를 입력하세요.')); - return instance - .delete(`/articles/${id}`) - .then((res) => { - console.log('삭제 성공, 상태 코드:', res.status); - return res.data; - }) - .catch((err) => { - console.log( - `deleteArticle 실패, 상태 코드:`, - err.response?.status ?? err.message - ); - throw err; - }); -} diff --git a/mission2/ElectronicProduct.js b/mission2/ElectronicProduct.js deleted file mode 100644 index d3e2de776..000000000 --- a/mission2/ElectronicProduct.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Product } from './product.js'; -/* 부모: Product - 자식: EletronicProduct */ -export class ElectronicProduct extends Product { - constructor( - name, - description, - price, - tags = [], - images = [], - favoriteCount = 0, - manufacturer = '' //제조사 - ) { - super(name, description, price, tags, images, favoriteCount); //부모 호출 - this.manufacturer = manufacturer; //제조사 상속 및 초기화 - } -} diff --git a/mission2/ProductService.js b/mission2/ProductService.js deleted file mode 100644 index a8de37348..000000000 --- a/mission2/ProductService.js +++ /dev/null @@ -1,119 +0,0 @@ -export async function getProductList(params = {}) { - try { - const url = new URL('https://panda-market-api-crud.vercel.app/products'); - Object.keys(params).forEach((key) => { - const value = params[key]; - if (value == null) return; - url.searchParams.append(key, params[key]); - }); - const res = await fetch(url); - if (!res.ok) { - const err = new Error(`서버 응답 오류, 상태 코드:${res.status}`); - err.status = res.status; - throw err; - } - const data = await res.json(); - return data; - } catch (error) { - console.error('getProductList 호출 실패', error); - throw error; - } -} - -export async function getProduct(id) { - try { - if (id == null) { - throw new Error(`조회할 id를 입력하세요.`); - } - const url = new URL( - `https://panda-market-api-crud.vercel.app/products/${id}` - ); - const res = await fetch(url); - if (!res.ok) { - const err = new Error(`서버 응답 오류, 상태 코드:${res.status}`); - err.status = res.status; - throw err; - } - const data = await res.json(); - return data; - } catch (error) { - console.error(`getProduct id 호출 실패`, error); - throw error; - } -} - -export async function createProduct(postData = {}) { - try { - const url = new URL(`https://panda-market-api-crud.vercel.app/products`); - const res = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(postData), - }); - if (!res.ok) { - const err = new Error(`서버 응답 오류, 상태코드:${res.status}`); - err.status = res.status; - throw err; - } - const data = await res.json(); - return data; - } catch (error) { - console.error(`createProduct 호출 실패`, error); - throw error; - } -} - -export async function patchProduct(id, patchData = {}) { - try { - if (id == null) { - throw new Error(`수정할 id를 입력하세요`); - } - const url = new URL( - `https://panda-market-api-crud.vercel.app/products/${id}` - ); - const res = await fetch(url, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(patchData), - }); - if (!res.ok) { - const err = new Error(`서버 응답 오류, 상태코드:${res.status}`); - err.status = res.status; - throw err; - } - const data = await res.json(); - return data; - } catch (error) { - console.error(`patchProduct 호출 실패`, error); - throw error; - } -} - -export async function deleteProduct(id) { - try { - if (id == null) { - throw new Error(`삭제할 id를 입력하세요.`); - } - const url = new URL( - `https://panda-market-api-crud.vercel.app/products/${id}` - ); - const res = await fetch(url, { - method: 'DELETE', - }); - if (!res.ok) { - const err = new Error(`서버 응답 오류, 상태코드:${res.status}`); - err.status = res.status; - throw err; - } - const data = await res.json(); - console.log('[삭제 성공]'); - return data; - } catch (error) { - console.error(`deleteProduct 호출 실패`, error); - throw error; - } -} diff --git a/mission2/main.js b/mission2/main.js deleted file mode 100644 index 64caed859..000000000 --- a/mission2/main.js +++ /dev/null @@ -1,141 +0,0 @@ -import AtcService from './ArticleService.js'; -import { Article } from './Article.js'; -import { ElectronicProduct } from './ElectronicProduct.js'; -import * as ProductService from './ProductService.js'; -import { Product } from './product.js'; - -/*vscode를 실행하고 articleService함수를 호출,제일 처음 node main.js를 터미널에 입력하면 -첫 회는 무조건 에러가 나고 그 다음 2회차부터 정상작동 되던데 왜 그런지 궁금합니다. 아래는 에러 내용 일부입니다. -axios 인스턴스로 url생성 타임아웃 2초가 너무 짧은은 상태에서 json을 파싱하지 못한 상태로 반환된 건가요? -왜 첫 회만 그렇고 2회차부터는 되는건지ㅠ... - -getArticleList 실패, 상태 코드: timeout of 2000ms exceeded - -node:internal/modules/run_main:123 - triggerUncaughtException( - ^ -AxiosError: timeout of 2000ms exceeded - at RedirectableRequest.handleRequestTimeout (file:///Users/apple/codeit-mission1/6-sprint-mission/SprintMission2/node_modules/axios/lib/adapters/http.js:675:16) - at RedirectableRequest.emit (node:events:519:28) - at Timeout. (/Users/apple/codeit-mission1/6-sprint-mission/SprintMission2/node_modules/follow-redirects/index.js:221:12) - at listOnTimeout (node:internal/timers:588:17) - at process.processTimers (node:internal/timers:523:7) - at Axios.request (file:///Users/apple/codeit-mission1/6-sprint-mission/SprintMission2/node_modules/axios/lib/core/Axios.js:45:41) - at async file:///Users/apple/codeit-mission1/6-sprint-mission/SprintMission2/main.js:6:17 { -*/ -async function atcService() { - console.log(`-------------GET/ArticleList-------------`); - const atcList = await AtcService.getArticleList({ - page: null, - pageSize: null, - keyword: null, - }); - console.log(atcList); - - console.log(`-------------GET/Article/id-------------`); - const atcId = await AtcService.getArticle(`4852`); - console.log(atcId); - - console.log(`-------------POST/ArticleData-------------`); - const atcData = { - title: `제 목`, - content: `내 용`, - image: 'https://ex.com', - }; - const postArticle = await AtcService.createArticle(atcData); - console.log(postArticle); - - console.log(`-------------PATCH/Article/id-------------`); - const atcPatch = await AtcService.patchArticle(4852, { - title: `제 목(수정)`, - }); - console.log(atcPatch); - - console.log(`-------------DELETE/Article/id-------------`); - const atcDeleteId = await AtcService.deleteArticle('5084'); - console.log(atcDeleteId); -} -/*저는 이번에 미션 진행하면서 크게 세 가지(클래스,클래스함수,호출부)의 성격을 가진 모듈들이 서로 어떤 관계성을 가지고 상호작용하는지 궁금했는데요. -클래스를 왜만들고 서비스함수를 만들어서 호출부에서 어떻게 다뤄야하는지 잘 이해가 안됐어요. 그래서 멘토링 때 말씀드렸던 -import/export해야할 상황이나 목적에 대해서 헷갈렸던 것 같습니다. - -모범답안을 보니 호출부에서 product클래스만 import해서 활용하고있는데 그 목적을 보면 상품의 종류를 나눠 다루기 위한 것으로 -이해됩니다. 반면 article함수 쪽을 확인하면 article 클래스를 활용하고 있지 않고있으면서 export하고있습니다. -이 article클래스가 어떻게 활용될 것인지 왜 export하고 있는지 궁금합니다. product 쪽도 마찬가지겠지만 상품의 -종류를 나누는 코드가 없었다면 article 클래스와 마찬가지로 그 모듈이 하는 역할에 대해 궁금했을 것입니다. - -단순히 코드만 보면 api를 통해 받아온 정보들을 class에 담아 검증, 캡슐화 등등 정리하고 배열하는 것 같은데 이해하는게 맞는지, -아니라면 어떻게 활용되는 것인지 궁금합니다. -(article 클래스는 이번 미션 단계에서 활용하지 않지만 다음 미션에서는 활용하기에 export 한 것인지 궁금합니다.)*/ - -/*prdtsModel 호출부는 학습하다 잘 모르겠는 도중에 모범답안이 나와, 확인 후에 코드 구동 원리를 이해하고자 했습니다. -호출하는 것까지는 어려움이 없었는데, 태그를 통한 상품 분류에서 많이 헤멘 것 같습니다. 상품을 배열로 놓고 순회하는 것, -for문에서 prdt는 단수형 prdts는 복수형같은 네이밍을 해야하는 것, 리터럴을 { list: prdts }로 지정해야하는 것, -이러한 부분들을 놓쳤던 것 같습니다. 이 부분들도 같이 설명해주시면 감사할 것 같습니다.*/ - -async function prdtsModel() { - console.log(`-------------GET/Products------------`); - const { list: prdts } = await ProductService.getProductList({ - page: null, - pageSize: null, - keyword: '', - }); - console.log(prdts); - const prdtList = []; - for (const prdt of prdts) { - let product; - if (prdt.tags.includes('전자제품')) { - product = new ElectronicProduct(prdt); - } else { - product = new Product(prdt); - } - prdtList.push(product); - } - console.log(prdtList); -} - -async function prdtService() { - console.log(`-------------GET/ProductList-------------`); - const prdtList = await ProductService.getProductList({ - page: null, - pageSize: null, - keyword: '', - }); - console.log(prdtList); - - console.log(`-------------GET/Product/id-------------`); - const prdtid = await ProductService.getProduct('2571'); - console.log(prdtid); - - console.log(`-------------POST/Product/Data-------------`); - const prdtPost = await ProductService.createProduct({ - name: '스피커', - description: '택배비 무료', - price: 40000, - tags: '전자제품', - images: 'https://example.com', - }); - console.log(prdtPost); - - console.log(`-------------PATCH/Product/Data-------------`); - const prdtPatch = await ProductService.patchProduct(2589, { - name: '스피커', - description: '택배비 유료', - price: 43000, - tags: '전자제품', - images: 'https://example.com', - }); - console.log(prdtPatch); - - console.log(`-------------DELETE/Product/Data-------------`); - const prdtDelete = await ProductService.deleteProduct(2622); - console.log(prdtDelete); -} -/* 모범 답안에서 호출부를 단순화하는 것 같아 보고 활용했습니다. */ -async function test() { - await atcService(); - await prdtsModel(); - await prdtService(); -} - -test(); diff --git a/mission2/package-lock.json b/mission2/package-lock.json deleted file mode 100644 index 439604311..000000000 --- a/mission2/package-lock.json +++ /dev/null @@ -1,318 +0,0 @@ -{ - "name": "6-sprint-mission", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "6-sprint-mission", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "asynckit": "^0.4.0", - "axios": "^1.12.2", - "call-bind-apply-helpers": "^1.0.2", - "combined-stream": "^1.0.8", - "delayed-stream": "^1.0.0", - "dunder-proto": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "follow-redirects": "^1.15.11", - "form-data": "^4.0.4", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0", - "mime-db": "^1.52.0", - "mime-types": "^2.1.35", - "proxy-from-env": "^1.1.0" - }, - "devDependencies": {} - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - } - } -} diff --git a/mission2/package.json b/mission2/package.json deleted file mode 100644 index 12104d51c..000000000 --- a/mission2/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "6-sprint-mission", - "version": "1.0.0", - "description": "", - "homepage": "https://github.com/chamysj/6-sprint-mission#readme", - "bugs": { - "url": "https://github.com/chamysj/6-sprint-mission/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/chamysj/6-sprint-mission.git" - }, - "license": "ISC", - "author": "", - "type": "module", - "main": "main.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "asynckit": "^0.4.0", - "axios": "^1.12.2", - "call-bind-apply-helpers": "^1.0.2", - "combined-stream": "^1.0.8", - "delayed-stream": "^1.0.0", - "dunder-proto": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "follow-redirects": "^1.15.11", - "form-data": "^4.0.4", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0", - "mime-db": "^1.52.0", - "mime-types": "^2.1.35", - "proxy-from-env": "^1.1.0" - }, - "devDependencies": {} -} diff --git a/mission2/product.js b/mission2/product.js deleted file mode 100644 index 9c646b8c4..000000000 --- a/mission2/product.js +++ /dev/null @@ -1,25 +0,0 @@ -export class Product { - #favoriteCount; //캡슐화 - constructor( - name, - description, - price, - tags = [], - images = [], - favoriteCount = 0 - ) { - this.name = name; //상품명 - this.description = description; //상품 설명 - this.price = price; //판매 가격 - this.tags = tags; //해시태그 - this.images = images; //이미지 - this.#favoriteCount = favoriteCount; //찜하기 수 - } - //찜하기 수 (증가) - favorite() { - this.#favoriteCount += 1; - } - getfavoriteCount() { - return this.#favoriteCount; - } -} diff --git a/mission2/readMe.md b/mission2/readMe.md deleted file mode 100644 index a42e1d622..000000000 --- a/mission2/readMe.md +++ /dev/null @@ -1,12 +0,0 @@ -# Node.js 6기 스프린트미션2 - -api 함수 구현하기 -git과 github 활용하기 - ---- - -### 스프린터 - -이름 : 최민수 -E-mail : chamysj@naver.com -제출일자 : 2025.10.17. diff --git a/mission3/src/controllers/articleController.js b/mission3/src/controllers/articleController.js deleted file mode 100644 index 17a5e4a62..000000000 --- a/mission3/src/controllers/articleController.js +++ /dev/null @@ -1,129 +0,0 @@ -import { CreateArticleStruct, PatchArticleStruct } from '../structs/articleStructs.js'; -import { create } from 'superstruct'; -import { PrismaClient } from '@prisma/client'; -import { IdParamsStruct } from '../structs/productStructs.js'; -import { CreateCommentStruct } from '../structs/commentStructs.js'; - -const prisma = new PrismaClient(); - -export async function validateCreateArticle(req, res) { - const data = create(req.body, CreateArticleStruct); - const article = await prisma.article.create({ data }); - return res.status(201).send(article); -} - -export async function validateGetArticle(req, res) { - const { id } = create(req.params, IdParamsStruct); - const article = await prisma.article.findUnique({ where: { id } }); - return res.send(article); -} - -export async function validatePatchArticle(req, res) { - const { id } = create(req.params, IdParamsStruct); - const data = create(req.body, PatchArticleStruct); - await prisma.article.findUnique({ where: { id } }); - const updateArticle = await prisma.article.update({ where: { id }, data }); - return res.json({ message: '수정에 성공했습니다.', data: updateArticle }); -} - -export async function validateDeleteArticle(req, res) { - const { id } = create(req.params, IdParamsStruct); - const article = await prisma.article.findUnique({ where: { id } }); - return res.json({ message: '삭제에 성공했습니다.', data: article }); -} - -export async function validateGetArticles(req, res) { - const { offset = 0, limit = 10, order = 'newest', includeWord = '' } = req.query; - let orderBy; - switch (order) { - case 'oldest': - orderBy = { createdAt: 'asc' }; - break; - case 'newest': - default: - orderBy = { createdAt: 'desc' }; - } - const findWord = String(includeWord || '').trim(); - const where = findWord - ? { - OR: [ - { title: { contains: findWord, mode: 'insensitive' } }, - { content: { contains: findWord, mode: 'insensitive' } }, - ], - } - : {}; - const articles = await prisma.article.findMany({ - where, - orderBy, - skip: parseInt(offset), - take: parseInt(limit), - }); - return res.send(articles); -} - -export async function validateCreateComment(req, res) { - const { id: articleId } = create(req.params, IdParamsStruct); //구조 분해 할당 - const { content, nickname } = create(req.body, CreateCommentStruct); - const nick = String(nickname ?? '').trim(); - if (!nick) { - return res - .status(400) //400은 요청이 잘못된 문법이거나, 유청값이 유효하지 않을 때 - .json({ message: 'nickname을 작성해주세요.' }); - } - const anon = await prisma.nickname.upsert({ - //nicknmae 컬럼에 @unique같은 제약이 있어야 upsert가 올바르게 작동함 - where: { nickname: nick }, // 스키마 필드명이 `nickname`일 때 - update: {}, - create: { nickname: nick }, - }); - const articleComment = await prisma.articleComment.create({ - data: { - content, - article: { connect: { id: articleId } }, // article 연결 , connect는 prisma의 관계 api임 - nickname: { connect: { id: anon.id } }, // nickname 연결 , connect는 의도를 명확히 함, 관계 대상의 레코드 존재여부를 명시적으로 체크하는 장점이 있음 - nicknameText: anon.nickname, //프리즈마 스튜디오에서 nickname확인하기 위함임 - }, - //select지정하지 않으면 기본 스칼라 필드 반환, include나 select는 관계를 포함함 - select: { - id: true, - content: true, - createdAt: true, - updatedAt: true, - nickname: true, - nicknameText: true, - }, - }); - return res.status(201).json({ message: '댓글 등록에 성공했습니다.', data: articleComment }); -} - -/*커서 페이지네이션이 무한 스크롤로 이해하고있습니다. 비유를 하자면 일종의 책갈피를 꽂아두고 이후에 생성된(desc니까 내림차순) -댓글을 불러오도록 하는 것으로 이해했습니다. 그런데 여기서 잘 이해가 되지 않는 것은 다음페이지를 불러오는 로직인데 hasmore로 -limit +1 해서, 있다면 불러오고 없다면 false를 반환하는 것, 다음 커서를 불러올 때 그 커서가 다시 레코드에 포함되니까 옵션으로 skip =1을 준것이 맞을까요 -그리고 마지막 커서는 자동으로 db를 통해 지정?되는 것으로 이해를 했는데요. 그것도 맞을까요? 이 기능이 prisma studio에서 showing에 해당하는 기능인가요? -ai로 학습하고 로직생각해보고 코드 수정 및 디벨롭 하다보니 가끔 이게 맞나 싶은 것들이 있어서 주석 남겨봅니다ㅠ -또 ai는 옵셔널체이닝, 널리시 코얼레싱?을 굉장이 많이 쓰더라구요. 실무에서도 많이 쓰이나요?*/ - -export async function getArticleComments(req, res) { - const { id: articleId } = create(req.params, IdParamsStruct); - const limit = Math.min(50, Math.max(1, parseInt(req.query.limit ?? '10', 10))); - const cursor = req.query.cursor ? String(req.query.cursor) : undefined; - const findOptions = { - where: { articleId }, - orderBy: [{ createdAt: 'desc' }, { id: 'desc' }], - take: limit + 1, // hasMore 판별용으로 하나 더 불러오기 - select: { - id: true, - content: true, - createdAt: true, - }, - }; - if (cursor) { - findOptions.cursor = { id: cursor }; - findOptions.skip = 1; // cursor로 지정된 레코드는 결과에서 제외 - } - const rows = await prisma.articleComment.findMany(findOptions); - const hasMore = rows.length > limit; - const results = hasMore ? rows.slice(0, limit) : rows; - const nextCursor = hasMore ? results[results.length - 1].id : null; - return res.json({ data: results, nextCursor, hasMore }); -} diff --git a/mission3/src/controllers/commentController.js b/mission3/src/controllers/commentController.js deleted file mode 100644 index 07cf62d1d..000000000 --- a/mission3/src/controllers/commentController.js +++ /dev/null @@ -1,67 +0,0 @@ -import { create } from 'superstruct'; -import { PrismaClient } from '@prisma/client'; -import { CommentIdStruct, PatchCommentStruct } from '../structs/commentStructs.js'; - -const prisma = new PrismaClient(); - -async function findCommentModel(id) { - const article = await prisma.articleComment.findUnique({ - where: { id }, - select: { id: true }, - }); - if (article) return 'article'; - - const product = await prisma.productComment.findUnique({ - where: { id }, - select: { id: true }, - }); - if (product) return 'product'; - - return null; -} - -export async function patchComment(req, res) { - const { id } = create(req.params, CommentIdStruct); - const { content } = create(req.body, PatchCommentStruct); - const trimmed = String(content ?? '').trim(); - const model = await findCommentModel(id); - const include = { nickname: { select: { nickname: true } } }; - const updated = - model === 'article' - ? await prisma.articleComment.update({ where: { id }, data: { content: trimmed }, include }) - : await prisma.productComment.update({ where: { id }, data: { content: trimmed }, include }); - const displayNickname = updated.nickname?.nickname ?? updated.nicknameText ?? null; - - return res.json({ - message: '수정됨', - data: { - id: updated.id, - content: updated.content, - nickname: displayNickname, - createdAt: updated.createdAt, - updatedAt: updated.updatedAt, - }, - }); -} - -export async function deleteComment(req, res) { - const { id } = create(req.params, CommentIdStruct); - const model = await findCommentModel(id); - const include = { nickname: { select: { nickname: true } } }; - const deleted = - model === 'article' - ? await prisma.articleComment.delete({ where: { id }, include }) - : await prisma.productComment.delete({ where: { id }, include }); - const displayNickname = deleted.nickname?.nickname ?? deleted.nicknameText ?? null; - - return res.json({ - message: '삭제됨', - data: { - id: deleted.id, - content: deleted.content, - nickname: displayNickname, - createdAt: deleted.createdAt, - updatedAt: deleted.updatedAt, - }, - }); -} diff --git a/mission3/src/controllers/errorController.js b/mission3/src/controllers/errorController.js deleted file mode 100644 index b6dc8a288..000000000 --- a/mission3/src/controllers/errorController.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Prisma } from '@prisma/client'; -import { StructError } from 'superstruct'; - -export function errorHandler(err, req, res, next) { - if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2025') { - return res.sendStatus(404); - } else if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') { - return res.status(400).send({ message: err.message }); - } else if (err.name === 'StructError' || err instanceof StructError) { - return res.status(400).send({ message: err.message }); - } else { - return res.status(500).send({ message: err.message }); - } -} diff --git a/mission3/src/controllers/imageController.js b/mission3/src/controllers/imageController.js deleted file mode 100644 index 44f189380..000000000 --- a/mission3/src/controllers/imageController.js +++ /dev/null @@ -1,5 +0,0 @@ -export async function imageUpload(req, res) { - const { filename } = req.file; - const path = `files/${filename}`; - return res.status(201).json({ message: '업로드 성공', path }); -} diff --git a/mission3/src/controllers/productController.js b/mission3/src/controllers/productController.js deleted file mode 100644 index c693d8171..000000000 --- a/mission3/src/controllers/productController.js +++ /dev/null @@ -1,121 +0,0 @@ -import { create } from 'superstruct'; -import { - CreateProductStruct, - IdParamsStruct, - PatchProductStruct, -} from '../structs/productStructs.js'; -import { PrismaClient } from '@prisma/client'; -import { CreateCommentStruct } from '../structs/commentStructs.js'; - -const prisma = new PrismaClient(); - -export async function validateCreateProduct(req, res) { - const data = create(req.body, CreateProductStruct); - const product = await prisma.product.create({ data }); - return res.status(201).send(product); -} - -export async function validateGetProduct(req, res) { - const { id } = create(req.params, IdParamsStruct); - const product = await prisma.product.findUnique({ where: { id } }); - return res.send(product); -} - -export async function validatePatchProduct(req, res) { - const { id } = create(req.params, IdParamsStruct); - const data = create(req.body, PatchProductStruct); - await prisma.product.findUnique({ where: { id } }); - const updateProduct = await prisma.product.update({ where: { id }, data }); - return res.json({ message: '수정에 성공했습니다.', data: updateProduct }); -} - -export async function validateDeleteProduct(req, res) { - const { id } = create(req.params, IdParamsStruct); - const product = await prisma.product.findUnique({ where: { id } }); - return res.json({ message: '삭제에 성공했습니다.', data: product }); -} - -export async function validateGetProducts(req, res) { - const { offset = 0, limit = 10, order = 'newest', includeWord = '' } = req.query; - let orderBy; - switch (order) { - case 'oldest': - orderBy = { createdAt: 'asc' }; - break; - case 'newest': - default: - orderBy = { createdAt: 'desc' }; - } - const findWord = String(includeWord || '').trim(); - const where = findWord - ? { - OR: [ - { name: { contains: findWord, mode: 'insensitive' } }, - { description: { contains: findWord, mode: 'insensitive' } }, - ], - } - : {}; - const products = await prisma.product.findMany({ - where, - orderBy, - skip: parseInt(offset), - take: parseInt(limit), - }); - return res.send(products); -} - -export async function validateCreateComment(req, res) { - const { id: productId } = create(req.params, IdParamsStruct); - const { content, nickname } = create(req.body, CreateCommentStruct); - const nick = String(nickname ?? '').trim(); - if (!nick) { - return res.status(400).json({ message: 'nickname을 작성해주세요.' }); - } - const anon = await prisma.nickname.upsert({ - where: { nickname: nick }, - update: {}, - create: { nickname: nick }, - }); - const productComment = await prisma.productComment.create({ - data: { - content, - product: { connect: { id: productId } }, - nickname: { connect: { id: anon.id } }, - nicknameText: anon.nickname, - }, - select: { - id: true, - content: true, - createdAt: true, - updatedAt: true, - nickname: true, - nicknameText: true, - }, - }); - return res.status(201).json({ message: '댓글 등록에 성공했습니다.', data: productComment }); -} - -export async function getProductComments(req, res) { - const { id: productId } = create(req.params, IdParamsStruct); - const limit = Math.min(50, Math.max(1, parseInt(req.query.limit ?? '10', 10))); - const cursor = req.query.cursor ? String(req.query.cursor) : undefined; - const findOptions = { - where: { productId }, - orderBy: [{ createdAt: 'desc' }, { id: 'desc' }], - take: limit + 1, - select: { - id: true, - content: true, - createdAt: true, - }, - }; - if (cursor) { - findOptions.cursor = { id: cursor }; - findOptions.skip = 1; - } - const rows = await prisma.productComment.findMany(findOptions); - const hasMore = rows.length > limit; - const results = hasMore ? rows.slice(0, limit) : rows; - const nextCursor = hasMore ? results[results.length - 1].id : null; - return res.json({ data: results, nextCursor, hasMore }); -} diff --git a/mission3/src/lib/asyncHandler.js b/mission3/src/lib/asyncHandler.js deleted file mode 100644 index e3762d3b1..000000000 --- a/mission3/src/lib/asyncHandler.js +++ /dev/null @@ -1,9 +0,0 @@ -export function asyncHandler(handler) { - return async function (req, res, next) { - try { - await handler(req, res); - } catch (error) { - next(error); - } - }; -} diff --git a/mission3/src/lib/constants.js b/mission3/src/lib/constants.js deleted file mode 100644 index 76513a384..000000000 --- a/mission3/src/lib/constants.js +++ /dev/null @@ -1,7 +0,0 @@ -import dotenv from 'dotenv'; -dotenv.config(); - -export const DATABASE_URL = process.env.DATABASE_URL; -export const PORT = process.env.PORT || 3000; -export const PUBLIC_PATH = 'uploads'; -export const STATIC_PATH = '/uploads'; diff --git a/mission3/src/main.js b/mission3/src/main.js deleted file mode 100644 index 001cbc452..000000000 --- a/mission3/src/main.js +++ /dev/null @@ -1,29 +0,0 @@ -import express from 'express'; -import { PrismaClient } from '@prisma/client'; -import cors from 'cors'; -import commentRouter from './routers/commentRouter.js'; -import productRouter from './routers/productRouter.js'; -import articleRouter from './routers/articleRouter.js'; -import { imageRouter } from './routers/imageRouter.js'; -import { PUBLIC_PATH, STATIC_PATH } from './lib/constants.js'; -import path from 'path'; -import { errorHandler } from './controllers/errorController.js'; - -const PORT = 3000; -const app = express(); - -app.use(express.json()); -app.use(cors()); -app.use(STATIC_PATH, express.static(path.resolve(process.cwd(), PUBLIC_PATH))); - -const prisma = new PrismaClient(); - -app.use('/articles', articleRouter); -app.use('/comments', commentRouter); -app.use('/products', productRouter); -app.use('/images', imageRouter); -app.use(errorHandler); - -app.listen(PORT, () => { - console.log('Server Started'); -}); diff --git a/mission3/src/prisma/migrations/20251027092554_base_schema/migration.sql b/mission3/src/prisma/migrations/20251027092554_base_schema/migration.sql deleted file mode 100644 index 7e0ed1bab..000000000 --- a/mission3/src/prisma/migrations/20251027092554_base_schema/migration.sql +++ /dev/null @@ -1,26 +0,0 @@ --- CreateEnum -CREATE TYPE "Tag" AS ENUM ('SPORTS', 'BEAUTY', 'FURNITURE', 'FASHION', 'ELECTRONICS'); - --- CreateTable -CREATE TABLE "Product" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "price" DOUBLE PRECISION NOT NULL, - "tags" "Tag" NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Product_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Article" ( - "id" TEXT NOT NULL, - "title" TEXT NOT NULL, - "content" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Article_pkey" PRIMARY KEY ("id") -); diff --git a/mission3/src/prisma/migrations/20251028062500_alter_schema/migration.sql b/mission3/src/prisma/migrations/20251028062500_alter_schema/migration.sql deleted file mode 100644 index 1526b1b5f..000000000 --- a/mission3/src/prisma/migrations/20251028062500_alter_schema/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ -/* - Warnings: - - - The values [BEAUTY] on the enum `Tag` will be removed. If these variants are still used in the database, this will fail. - -*/ --- AlterEnum -BEGIN; -CREATE TYPE "Tag_new" AS ENUM ('SPORTS', 'INSTRUMENTS', 'FURNITURE', 'FASHION', 'ELECTRONICS'); -ALTER TABLE "Product" ALTER COLUMN "tags" TYPE "Tag_new" USING ("tags"::text::"Tag_new"); -ALTER TYPE "Tag" RENAME TO "Tag_old"; -ALTER TYPE "Tag_new" RENAME TO "Tag"; -DROP TYPE "Tag_old"; -COMMIT; diff --git a/mission3/src/prisma/migrations/20251031044353_add_model_nickname_and_comments/migration.sql b/mission3/src/prisma/migrations/20251031044353_add_model_nickname_and_comments/migration.sql deleted file mode 100644 index 996e9263e..000000000 --- a/mission3/src/prisma/migrations/20251031044353_add_model_nickname_and_comments/migration.sql +++ /dev/null @@ -1,48 +0,0 @@ --- CreateTable -CREATE TABLE "ProductComment" ( - "id" TEXT NOT NULL, - "content" TEXT NOT NULL, - "nicknameId" TEXT, - "productId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ProductComment_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ArticleComment" ( - "id" TEXT NOT NULL, - "content" TEXT NOT NULL, - "nicknameId" TEXT, - "articleId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "ArticleComment_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Nickname" ( - "id" TEXT NOT NULL, - "nickname" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Nickname_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Nickname_nickname_key" ON "Nickname"("nickname"); - --- AddForeignKey -ALTER TABLE "ProductComment" ADD CONSTRAINT "ProductComment_nicknameId_fkey" FOREIGN KEY ("nicknameId") REFERENCES "Nickname"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ProductComment" ADD CONSTRAINT "ProductComment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ArticleComment" ADD CONSTRAINT "ArticleComment_nicknameId_fkey" FOREIGN KEY ("nicknameId") REFERENCES "Nickname"("id") ON DELETE SET NULL ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ArticleComment" ADD CONSTRAINT "ArticleComment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/mission3/src/prisma/migrations/20251031080723_add_nickname_text/migration.sql b/mission3/src/prisma/migrations/20251031080723_add_nickname_text/migration.sql deleted file mode 100644 index 813667578..000000000 --- a/mission3/src/prisma/migrations/20251031080723_add_nickname_text/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- AlterTable -ALTER TABLE "ArticleComment" ADD COLUMN "nicknameText" TEXT; - --- AlterTable -ALTER TABLE "ProductComment" ADD COLUMN "nicknameText" TEXT; diff --git a/mission3/src/prisma/migrations/migration_lock.toml b/mission3/src/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92c2..000000000 --- a/mission3/src/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/mission3/src/prisma/mock.js b/mission3/src/prisma/mock.js deleted file mode 100644 index 3db055735..000000000 --- a/mission3/src/prisma/mock.js +++ /dev/null @@ -1,358 +0,0 @@ -export const PRODUCTS = [ - { - id: '6f435dde-97e7-4810-9199-69aaae5ae0a5', - name: '맥북 에어2', - description: '2025년 형', - price: 800000, - tags: 'ELECTRONICS', - createdAt: '2025-10-28T05:22:55.473Z', - updatedAt: '2025-10-28T05:22:55.473Z', - }, - { - id: '6b3d18c5-0e54-4ce8-860e-35fc9510a22f', - name: '래빗 체어', - description: '스테파노 지오반노니가 디자인한 제품', - price: 200000, - tags: 'FURNITURE', - createdAt: '2025-10-28T05:30:09.501Z', - updatedAt: '2025-10-28T05:30:09.501Z', - }, - { - id: 'd3f7e5ee-a1b7-45ff-9841-16f8a259ecf6', - name: '3단 수납 침대', - description: '템바보드의 고급스러움과 패브릭의 아늑함을 가득 담은 침대', - price: 220000, - tags: 'FURNITURE', - createdAt: '2025-10-28T05:38:43.363Z', - updatedAt: '2025-10-28T05:38:43.363Z', - }, - { - id: 'fe2b99ce-9cff-4068-a3fc-6c12506984a4', - name: '겟우드 침대 더블', - description: '튼튼한 철제프레임에 심할하고 내추럴한 나무 느낌을 더한 고급스러운 침대', - price: 140000, - tags: 'FURNITURE', - createdAt: '2025-10-28T05:40:57.994Z', - updatedAt: '2025-10-28T05:40:57.994Z', - }, - { - id: '626c8adf-90fa-4722-9dea-1578061235bb', - name: '시몬스 자스민 침대 매트리스', - description: '최상의 편안함과 여유로움', - price: 1720000, - tags: 'FURNITURE', - createdAt: '2025-10-28T05:44:22.667Z', - updatedAt: '2025-10-28T05:44:22.667Z', - }, - { - id: 'e717e3ab-3ab3-4312-a437-5ca9d9e900cf', - name: '누베스 아쿠아텍스 패브릭 2인용 소파', - description: '당신의 지친 몸과 마음을 포근히 감싸줄 침대 같은 편안함을 제공합니다.', - price: 230000, - tags: 'FURNITURE', - createdAt: '2025-10-28T05:46:24.848Z', - updatedAt: '2025-10-28T05:46:24.848Z', - }, - { - id: 'b2978e84-6622-4f3f-a2a5-a815be5a743a', - name: 'Whale chair', - description: '실내외용으로 사용 가능하며, 고래 꼬리를 잡고 쉽게 움직일 수 있는 의자입니다.', - price: 350000, - tags: 'FURNITURE', - createdAt: '2025-10-28T05:48:25.265Z', - updatedAt: '2025-10-28T05:48:25.265Z', - }, - { - id: '29d7cbdc-1942-4a88-a79b-471ced47a7aa', - name: '루아즈 가정용 턱걸이 바', - description: '무틀 손상없이 강력한 밀착력!', - price: 9000, - tags: 'SPORTS', - createdAt: '2025-10-28T05:54:00.424Z', - updatedAt: '2025-10-28T05:54:00.424Z', - }, - { - id: '49d41154-b217-411f-9a6b-e103526b2a72', - name: '코멧 스포츠 컴포트 폼롤러', - description: '탄탄한 쿠션감이 주는 편안한 자극!', - price: 12000, - tags: 'SPORTS', - createdAt: '2025-10-28T06:01:24.931Z', - updatedAt: '2025-10-28T06:01:24.931Z', - }, - { - id: '0ef28c73-4e93-495c-a272-1a06a1f35a3a', - name: '가정용 철봉 딥스바', - description: - '직접 조립하며 느낄 수 있는 즐거움과 견고함, 합리성을 느낄 수 있는 조립상품입니다.', - price: 140000, - tags: 'SPORTS', - createdAt: '2025-10-28T06:03:34.306Z', - updatedAt: '2025-10-28T06:03:34.306Z', - }, - { - id: 'ed210053-ae39-4202-86d1-5880423e5bd1', - name: '접이식 실내 자전거', - description: '핸들을 없애고, 코어근육을 사용해 기존 제품보다 빠르게 칼로리를 소모합니다.', - price: 400000, - tags: 'SPORTS', - createdAt: '2025-10-28T06:05:51.244Z', - updatedAt: '2025-10-28T06:05:51.244Z', - }, - { - id: '07bd8550-63ae-44b3-b6a0-bf128831f0a6', - name: '아디다스 축구화', - description: '남녀공용, 인조잔디에 적합한 아웃솔', - price: 64000, - tags: 'SPORTS', - createdAt: '2025-10-28T06:07:43.974Z', - updatedAt: '2025-10-28T06:07:43.974Z', - }, - { - id: '8d88ed1f-e49a-4481-8394-cdb54a1c604e', - name: '아이워너 ab코어 휠', - description: '밀고 당기는 간단한 동작으로 팔,어깨,복근 운동을 한번에 할 수 있습니다.', - price: 8000, - tags: 'SPORTS', - createdAt: '2025-10-28T06:09:44.963Z', - updatedAt: '2025-10-28T06:09:44.963Z', - }, - { - id: '85a474ea-a3f5-4f6d-ac4e-756c309395bb', - name: '바이올린 초보자 기본형', - description: '수제 제작 성인용 바이올린!', - price: 130000, - tags: 'INSTRUMENTS', - createdAt: '2025-10-28T06:25:42.708Z', - updatedAt: '2025-10-28T06:25:42.708Z', - }, - { - id: '11ca3bc8-f928-4dd6-a947-1890df68fbd6', - name: '시어몬 트럼본', - description: '전공용과 연주용으로 전혀 손색 없는 제품', - price: 8000000, - tags: 'INSTRUMENTS', - createdAt: '2025-10-28T06:27:24.872Z', - updatedAt: '2025-10-28T06:27:24.872Z', - }, - { - id: '87df3958-6a32-476e-a2e9-770b6454ba68', - name: '어쿠스틱 기타', - description: '연주와 관리에 필요한 모든 액세서리 증정', - price: 150000, - tags: 'INSTRUMENTS', - createdAt: '2025-10-28T06:29:44.725Z', - updatedAt: '2025-10-28T06:29:44.725Z', - }, - { - id: '424d4cac-1ab0-43d8-ba0c-e1ebe008893a', - name: '칸타빌 통기타', - description: '풀 패키지 구성품 증정', - price: 58000, - tags: 'INSTRUMENTS', - createdAt: '2025-10-28T06:31:03.952Z', - updatedAt: '2025-10-28T06:31:03.952Z', - }, - { - id: 'e056581c-9ac9-4d6d-b9e6-ab3731792cda', - name: '하모니카', - description: '삼익악기 24홀', - price: 38000, - tags: 'INSTRUMENTS', - createdAt: '2025-10-28T06:32:18.954Z', - updatedAt: '2025-10-28T06:32:18.954Z', - }, - { - id: '9f37be61-b631-4513-a7ed-2698aee50626', - name: '트럼펫', - description: '일본 야마하 트럼펫 입문용', - price: 138000, - tags: 'INSTRUMENTS', - createdAt: '2025-10-28T06:33:35.838Z', - updatedAt: '2025-10-28T06:33:35.838Z', - }, - { - id: '22e36934-2987-479e-9b35-fb497e2cbb26', - name: '양털 하이넥 패딩', - description: '기능성 방수, 방풍, 숏 잠바', - price: 23000, - tags: 'FASHION', - createdAt: '2025-10-28T06:35:13.550Z', - updatedAt: '2025-10-28T06:35:13.550Z', - }, - { - id: 'e92c1436-c5f4-4ca2-826e-0b2331123d19', - name: '롱패딩', - description: '가볍고 따뜻한 롱패딩!', - price: 55000, - tags: 'FASHION', - createdAt: '2025-10-28T06:37:00.483Z', - updatedAt: '2025-10-28T06:37:00.483Z', - }, - { - id: '495ca8e4-7639-4089-b4d1-0b7a93837179', - name: '긴팔 니트', - description: '가볍고 따뜻하며 맨살에 입어도 부드럽습니다.', - price: 55000, - tags: 'FASHION', - createdAt: '2025-10-28T06:38:40.449Z', - updatedAt: '2025-10-28T06:38:40.449Z', - }, - { - id: 'd6717179-e9d0-4970-9e03-749889ba6e54', - name: '빅사이즈 니트', - description: '빅사이즈 외출용 간절기 니트!', - price: 22000, - tags: 'FASHION', - createdAt: '2025-10-28T06:40:26.092Z', - updatedAt: '2025-10-28T06:40:26.092Z', - }, - { - id: '40d0556c-2f2d-471f-bcc8-a2d2cfab791e', - name: '패딩 바지', - description: '겨울용 방한 바지', - price: 90000, - tags: 'FASHION', - createdAt: '2025-10-28T06:41:14.576Z', - updatedAt: '2025-10-28T06:41:14.576Z', - }, - { - id: '9426b0e2-61ef-4728-8c86-ef6279c52d66', - name: '경량 패딩', - description: '남녀공용, 가볍고 따뜻합니다.', - price: 42000, - tags: 'FASHION', - createdAt: '2025-10-28T06:42:08.467Z', - updatedAt: '2025-10-28T06:42:08.467Z', - }, - { - id: '82980f81-c3d7-42cd-bec9-4cd2f09ca71b', - name: '런닝화', - description: '남성용, 가볍고 발편한 런닝화.', - price: 35000, - tags: 'FASHION', - createdAt: '2025-10-28T06:43:36.147Z', - updatedAt: '2025-10-28T06:43:36.147Z', - }, - { - id: '5c8c75e9-0195-48e4-80bf-5f6d7ed0c438', - name: '벙거지 모자', - description: '넓은 챙, 자외선 차단 벙거지 모자', - price: 15000, - tags: 'FASHION', - createdAt: '2025-10-28T06:45:20.211Z', - updatedAt: '2025-10-28T06:45:20.211Z', - }, - { - id: '58bdc83f-79da-4dd2-9c6a-18dad5503ccc', - name: '멀티 쿠커', - description: '밥솥, 전자렌지, 오븐, 에어프라이, 찜기, 그릴 기능이 지원됩니다.', - price: 310000, - tags: 'ELECTRONICS', - createdAt: '2025-10-28T06:47:37.831Z', - updatedAt: '2025-10-28T06:47:37.831Z', - }, - { - id: '548b4025-972e-49b7-8b42-95057d95f182', - name: '마사지기', - description: '가벼운 무선 목 어깨 마사지기', - price: 30000, - tags: 'ELECTRONICS', - createdAt: '2025-10-28T06:48:34.181Z', - updatedAt: '2025-10-28T06:48:34.181Z', - }, - { - id: '959cee34-ef5d-4289-8d84-c5e7f48d5c61', - name: '스팀다리미', - description: '작지만 스팀효과는 그대로, 빠르고 간편하게 사용가능합니다.', - price: 25000, - tags: 'ELECTRONICS', - createdAt: '2025-10-28T06:50:32.047Z', - updatedAt: '2025-10-28T06:50:32.047Z', - }, - { - id: 'ce9887e7-0dc5-4dee-a798-bf3e948cec01', - name: '즉석 라면 조리기', - description: '가정용, 한강 라면 끓이는 기계', - price: 190000, - tags: 'ELECTRONICS', - createdAt: '2025-10-28T06:52:07.348Z', - updatedAt: '2025-10-28T06:52:07.348Z', - }, - { - id: '64a3067e-668b-4dd9-8d9c-d55042789db8', - name: '무선 주전자', - description: '360도 회전판, 과열 방지 기능 탑재', - price: 17000, - tags: 'ELECTRONICS', - createdAt: '2025-10-28T06:54:45.455Z', - updatedAt: '2025-10-28T06:54:45.455Z', - }, -]; - -export const ARTICLES = [ - { - id: '23b7dc34-7a6f-48a0-80b6-9a3503a3dddf', - title: '피곤하당', - content: '오늘은 좀 일찍 자야겠다.', - createdAt: '2025-10-28T07:36:13.410Z', - updatedAt: '2025-10-28T07:36:13.410Z', - }, - { - id: '187e0caf-288f-42e8-9e72-2945abdcd42f', - title: '배고파', - content: '오늘 저녁은 뭐먹지???????', - createdAt: '2025-10-28T07:41:42.085Z', - updatedAt: '2025-10-28T07:41:42.085Z', - }, - { - id: '2ddae395-27a8-4291-9152-dd6aa23e2f10', - title: '저녁 메뉴 추천 좀', - content: '이미 치킨 시킴', - createdAt: '2025-10-28T07:42:27.872Z', - updatedAt: '2025-10-28T07:42:27.872Z', - }, - { - id: 'c467c8d1-a336-4f7c-b1e2-1cda7718093d', - title: '오운완', - content: '운동만이 살길', - createdAt: '2025-10-28T07:43:58.997Z', - updatedAt: '2025-10-28T07:43:58.997Z', - }, - { - id: 'd5b27e43-fe7e-41f4-a21f-7f243fb17b30', - title: '강아지 식이알러지', - content: '우리 똥강아지 식이알러지가 있어서 괜찮은 사료 브랜드 추천 좀', - createdAt: '2025-10-28T07:45:17.039Z', - updatedAt: '2025-10-28T07:45:17.039Z', - }, - { - id: 'b76a7983-e79a-4dfa-a17f-bfa7d52e9fe3', - title: '강아지 실외배변', - content: - '똥강아지가 실외배변만 하는데 실내에서도 뉘이고 싶어요ㅠ 저랑 같은 경험 있으신 분 조언 좀 해주세요.', - createdAt: '2025-10-28T07:46:49.332Z', - updatedAt: '2025-10-28T07:46:49.332Z', - }, - { - id: '1569c142-ccd9-48c5-bdee-958bdc06379e', - title: '아 사기꾼이네!', - content: '중고마켓에서 물건 샀는데 벽돌왔음. 신고가능?', - createdAt: '2025-10-28T07:47:54.099Z', - updatedAt: '2025-10-28T07:47:54.099Z', - }, - { - id: '7d1476dd-e6fb-439f-aae0-8da282a5be9f', - title: '헐...나도 사기당함', - content: '나도 며칠 전에 사기당함..같은 판매잔가????', - createdAt: '2025-10-28T07:48:40.353Z', - updatedAt: '2025-10-28T07:48:40.353Z', - }, - { - id: '2898a4a2-41db-4e51-b9a7-adfbd6d77040', - title: '님들 따뜻한 패딩 추천 좀', - content: '가격은 10만원 이내로 이쁜걸루다가!', - createdAt: '2025-10-28T07:49:11.660Z', - updatedAt: '2025-10-28T07:49:11.660Z', - }, -]; diff --git a/mission3/src/prisma/schema.prisma b/mission3/src/prisma/schema.prisma deleted file mode 100644 index 9e665549f..000000000 --- a/mission3/src/prisma/schema.prisma +++ /dev/null @@ -1,72 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model Product { - id String @id @default(uuid()) - name String - description String? //컬럼이 null을 허용 - price Float - tags Tag - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt //자동갱신되기 때문에 updatedat사용 default now 사용하면 생성은 되지만 이후 업데이트 시 수동으로 덮어써야 함 - comments ProductComment[] -} - -//updatedAt을 쓰면 디버깅에 대한 이점이 있음, 신경쓰지 않는다면 DateTime?을 써도 됨. 권장되지는 않는 듯. -model Article { - id String @id @default(uuid()) - title String //글자 제한? - content String //글자 제한? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - comments ArticleComment[] -} - -//닉네임이 공백을 허용했기 때문에 struct에서 변수 제한해야 됨 -model ProductComment { - id String @id @default(uuid()) - content String - nickname Nickname? @relation(fields: [nicknameId], references: [id], onDelete: SetNull) - nicknameId String? - nicknameText String? - product Product @relation(fields: [productId], references: [id], onDelete: Cascade) - productId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model ArticleComment { - id String @id @default(uuid()) - content String - nickname Nickname? @relation(fields: [nicknameId], references: [id], onDelete: SetNull) - nicknameId String? - nicknameText String? - article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) - articleId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -//닉네임을 고유로 가지며 중복 글 작성 가능 -model Nickname { - id String @id @default(uuid()) - nickname String @unique - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - productComments ProductComment[] - articleComments ArticleComment[] -} - -enum Tag { - SPORTS - INSTRUMENTS - FURNITURE - FASHION - ELECTRONICS -} diff --git a/mission3/src/prisma/seed.js b/mission3/src/prisma/seed.js deleted file mode 100644 index 53e4e9908..000000000 --- a/mission3/src/prisma/seed.js +++ /dev/null @@ -1,26 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { ARTICLES, PRODUCTS } from './mock.js'; - -const prisma = new PrismaClient(); - -async function main() { - await prisma.article.deleteMany(); - await prisma.product.deleteMany(); - await prisma.article.createMany({ - data: ARTICLES, - skipDuplicates: true, - }); - await prisma.product.createMany({ - data: PRODUCTS, - skipDuplicates: true, - }); -} - -main() - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (e) => { - await prisma.$disconnect(); - process.exit(1); - }); diff --git a/mission3/src/routers/articleRouter.js b/mission3/src/routers/articleRouter.js deleted file mode 100644 index 72b3a40a4..000000000 --- a/mission3/src/routers/articleRouter.js +++ /dev/null @@ -1,22 +0,0 @@ -import express from 'express'; -import { asyncHandler } from '../lib/asyncHandler.js'; -import { - validateCreateArticle, - validateDeleteArticle, - validateGetArticle, - validateGetArticles, - validatePatchArticle, - validateCreateComment, - getArticleComments, -} from '../controllers/articleController.js'; - -const articleRouter = express.Router(); - -articleRouter.post('/', asyncHandler(validateCreateArticle)); -articleRouter.get('/:id', asyncHandler(validateGetArticle)); -articleRouter.patch('/:id', asyncHandler(validatePatchArticle)); -articleRouter.delete('/:id', asyncHandler(validateDeleteArticle)); -articleRouter.get('/', asyncHandler(validateGetArticles)); -articleRouter.post('/:id/comments', asyncHandler(validateCreateComment)); -articleRouter.get('/:id/comments', asyncHandler(getArticleComments)); -export default articleRouter; diff --git a/mission3/src/routers/commentRouter.js b/mission3/src/routers/commentRouter.js deleted file mode 100644 index 4a295033c..000000000 --- a/mission3/src/routers/commentRouter.js +++ /dev/null @@ -1,10 +0,0 @@ -import express from 'express'; -import { asyncHandler } from '../lib/asyncHandler.js'; -import { deleteComment, patchComment } from '../controllers/commentController.js'; - -const commentRouter = express.Router(); - -commentRouter.patch('/:id', asyncHandler(patchComment)); -commentRouter.delete('/:id', asyncHandler(deleteComment)); - -export default commentRouter; diff --git a/mission3/src/routers/imageRouter.js b/mission3/src/routers/imageRouter.js deleted file mode 100644 index 979ea7f29..000000000 --- a/mission3/src/routers/imageRouter.js +++ /dev/null @@ -1,11 +0,0 @@ -import express from 'express'; -import { asyncHandler } from '../lib/asyncHandler.js'; -import multer from 'multer'; -import { imageUpload } from '../controllers/imageController.js'; - -const imageRouter = express.Router(); -const upload = multer({ dest: 'uploads/' }); - -imageRouter.post('/', upload.single('attachment'), asyncHandler(imageUpload)); - -export { imageRouter }; diff --git a/mission3/src/routers/productRouter.js b/mission3/src/routers/productRouter.js deleted file mode 100644 index 09d015d6a..000000000 --- a/mission3/src/routers/productRouter.js +++ /dev/null @@ -1,22 +0,0 @@ -import express from 'express'; -import { asyncHandler } from '../lib/asyncHandler.js'; -import { - validateCreateProduct, - validateDeleteProduct, - validateGetProduct, - validateGetProducts, - validatePatchProduct, - validateCreateComment, - getProductComments, -} from '../controllers/productController.js'; - -const productRouter = express.Router(); - -productRouter.post('/', asyncHandler(validateCreateProduct)); -productRouter.get('/:id', asyncHandler(validateGetProduct)); -productRouter.patch('/:id', asyncHandler(validatePatchProduct)); -productRouter.delete('/:id', asyncHandler(validateDeleteProduct)); -productRouter.get('/', asyncHandler(validateGetProducts)); -productRouter.post('/:id/comments', asyncHandler(validateCreateComment)); -productRouter.get('/:id/comments', asyncHandler(getProductComments)); -export default productRouter; diff --git a/mission3/src/structs/articleStructs.js b/mission3/src/structs/articleStructs.js deleted file mode 100644 index 89aedff74..000000000 --- a/mission3/src/structs/articleStructs.js +++ /dev/null @@ -1,8 +0,0 @@ -import * as s from 'superstruct'; - -export const CreateArticleStruct = s.object({ - title: s.size(s.string(), 1, 100), - content: s.size(s.string(), 1, 1200), -}); - -export const PatchArticleStruct = s.partial(CreateArticleStruct); diff --git a/mission3/src/structs/commentStructs.js b/mission3/src/structs/commentStructs.js deleted file mode 100644 index acde524a6..000000000 --- a/mission3/src/structs/commentStructs.js +++ /dev/null @@ -1,17 +0,0 @@ -import * as s from 'superstruct'; -import isUuid from 'is-uuid'; - -export const CreateCommentStruct = s.object({ - nickname: s.size(s.string(), 2, 10), - content: s.size(s.string(), 1, 300), -}); - -export const PatchCommentStruct = s.partial(CreateCommentStruct); - -export const GetCommnetList = s.object({ - id: s.define('Uuid', (value) => isUuid.v4(value)), - nickname: s.size(s.string(), 2, 10), - content: s.size(s.string(), 1, 300), -}); - -export const CommentIdStruct = s.partial(GetCommnetList); diff --git a/mission3/src/structs/productStructs.js b/mission3/src/structs/productStructs.js deleted file mode 100644 index b849d3b42..000000000 --- a/mission3/src/structs/productStructs.js +++ /dev/null @@ -1,17 +0,0 @@ -import * as s from 'superstruct'; -import isUuid from 'is-uuid'; - -const TAGS = ['SPORTS', 'INSTRUMENTS', 'FURNITURE', 'FASHION', 'ELECTRONICS']; - -export const CreateProductStruct = s.object({ - name: s.size(s.string(), 1, 60), - description: s.size(s.string(), 1, 900), - price: s.min(s.number(), 0), - tags: s.enums(TAGS), -}); - -export const IdParamsStruct = s.object({ - id: s.define('Uuid', (value) => isUuid.v4(value)), //v4형식의 uuid 유효성 검증 -}); - -export const PatchProductStruct = s.partial(CreateProductStruct); diff --git a/mission9-10-11/.env.example b/mission9-10-11/.env.example new file mode 100644 index 000000000..9a38c69ce --- /dev/null +++ b/mission9-10-11/.env.example @@ -0,0 +1,8 @@ +DATABASE_URL="postgresql://user:password@localhost:5432/dbname?schema=public" +NODE_ENV=development +PORT=3000 +JWT_ACCESS_TOKEN_SECRET=your_access_token_secret_key +JWT_REFRESH_TOKEN_SECRET=your_refresh_token_secret_key +AWS_ACCESS_KEY_ID=your_aws_access_key_id +AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key +UPLOAD_PROVIDER=local \ No newline at end of file diff --git a/mission9-10-11/.env.production.example b/mission9-10-11/.env.production.example new file mode 100644 index 000000000..eb36d25b5 --- /dev/null +++ b/mission9-10-11/.env.production.example @@ -0,0 +1,11 @@ +NODE_ENV=production +UPLOAD_PROVIDER=s3 +AWS_REGION=your_aws_region +AWS_S3_BUCKET=your_s3_bucket_name +AWS_ACCESS_KEY_ID=your_aws_access_key_id +AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key +DATABASE_URL="postgresql://user:password@localhost:5432/dbname?schema=public" +//데이터 베이스 유알엘은 rds로 바꾸기 +PORT=3000 +JWT_ACCESS_TOKEN_SECRET=your_access_token_secret_key +JWT_REFRESH_TOKEN_SECRET=your_refresh_token_secret_key diff --git a/mission9/.env.test.example b/mission9-10-11/.env.test.example similarity index 100% rename from mission9/.env.test.example rename to mission9-10-11/.env.test.example diff --git a/mission9/.prettierrc b/mission9-10-11/.prettierrc similarity index 100% rename from mission9/.prettierrc rename to mission9-10-11/.prettierrc diff --git a/mission9/coverage/base.css b/mission9-10-11/coverage/base.css similarity index 100% rename from mission9/coverage/base.css rename to mission9-10-11/coverage/base.css diff --git a/mission9/coverage/block-navigation.js b/mission9-10-11/coverage/block-navigation.js similarity index 100% rename from mission9/coverage/block-navigation.js rename to mission9-10-11/coverage/block-navigation.js diff --git a/mission9/coverage/clover.xml b/mission9-10-11/coverage/clover.xml similarity index 100% rename from mission9/coverage/clover.xml rename to mission9-10-11/coverage/clover.xml diff --git a/mission9/coverage/controllers/articlesController.ts.html b/mission9-10-11/coverage/controllers/articlesController.ts.html similarity index 100% rename from mission9/coverage/controllers/articlesController.ts.html rename to mission9-10-11/coverage/controllers/articlesController.ts.html diff --git a/mission9/coverage/controllers/commentsController.ts.html b/mission9-10-11/coverage/controllers/commentsController.ts.html similarity index 100% rename from mission9/coverage/controllers/commentsController.ts.html rename to mission9-10-11/coverage/controllers/commentsController.ts.html diff --git a/mission9/coverage/controllers/errorController.ts.html b/mission9-10-11/coverage/controllers/errorController.ts.html similarity index 100% rename from mission9/coverage/controllers/errorController.ts.html rename to mission9-10-11/coverage/controllers/errorController.ts.html diff --git a/mission9/coverage/controllers/imagesController.ts.html b/mission9-10-11/coverage/controllers/imagesController.ts.html similarity index 100% rename from mission9/coverage/controllers/imagesController.ts.html rename to mission9-10-11/coverage/controllers/imagesController.ts.html diff --git a/mission9/coverage/controllers/index.html b/mission9-10-11/coverage/controllers/index.html similarity index 100% rename from mission9/coverage/controllers/index.html rename to mission9-10-11/coverage/controllers/index.html diff --git a/mission9/coverage/controllers/notificationController.ts.html b/mission9-10-11/coverage/controllers/notificationController.ts.html similarity index 100% rename from mission9/coverage/controllers/notificationController.ts.html rename to mission9-10-11/coverage/controllers/notificationController.ts.html diff --git a/mission9/coverage/controllers/productsController.ts.html b/mission9-10-11/coverage/controllers/productsController.ts.html similarity index 100% rename from mission9/coverage/controllers/productsController.ts.html rename to mission9-10-11/coverage/controllers/productsController.ts.html diff --git a/mission9/coverage/controllers/usersController.ts.html b/mission9-10-11/coverage/controllers/usersController.ts.html similarity index 100% rename from mission9/coverage/controllers/usersController.ts.html rename to mission9-10-11/coverage/controllers/usersController.ts.html diff --git a/mission9/coverage/coverage-final.json b/mission9-10-11/coverage/coverage-final.json similarity index 100% rename from mission9/coverage/coverage-final.json rename to mission9-10-11/coverage/coverage-final.json diff --git a/mission9/coverage/favicon.png b/mission9-10-11/coverage/favicon.png similarity index 100% rename from mission9/coverage/favicon.png rename to mission9-10-11/coverage/favicon.png diff --git a/mission9/coverage/index.html b/mission9-10-11/coverage/index.html similarity index 96% rename from mission9/coverage/index.html rename to mission9-10-11/coverage/index.html index c5aa52d70..86bf3c5c7 100644 --- a/mission9/coverage/index.html +++ b/mission9-10-11/coverage/index.html @@ -84,13 +84,13 @@

All files

100% - 26/26 + 25/25 100% 0/0 100% 0/0 100% - 26/26 + 25/25 @@ -129,13 +129,13 @@

All files

100% - 69/69 + 70/70 100% 0/0 100% 0/0 100% - 69/69 + 70/70 @@ -161,7 +161,7 @@

All files