diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..ff96bde0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +dist +uploads +.git +.env +*.jpg +*.png +infra +client-demo +__tests__ +.devcontainer +generated diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..4038437f --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,46 @@ +name: CI - Test + +on: + pull_request: + branches: ["*"] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: panda + POSTGRES_PASSWORD: panda1234 + POSTGRES_DB: panda_market_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U panda -d panda_market_test" + --health-interval 5s + --health-timeout 3s + --health-retries 5 + + env: + DATABASE_URL: postgresql://panda:panda1234@localhost:5432/panda_market_test + JWT_ACCESS_SECRET: test-access-secret + JWT_REFRESH_SECRET: test-refresh-secret + NODE_ENV: test + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - run: npx prisma generate + + - run: npx prisma migrate deploy + + - run: npm run build diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 00000000..0439fe04 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,39 @@ +name: CD - Deploy to AWS + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - run: npx prisma generate + + - run: npm run build + + - name: Deploy to EC2 + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + cd ~/sprint-mission- + git pull origin main + npm ci + npx prisma generate + npx prisma migrate deploy + npm run build + pm2 restart pandamarket-api || pm2 start npm --name pandamarket-api -- run start + pm2 save diff --git a/ArticleService.js b/ArticleService.js deleted file mode 100644 index f0b3b8b5..00000000 --- a/ArticleService.js +++ /dev/null @@ -1,116 +0,0 @@ -// ArticleService.js -import axios from "axios"; -import { Article } from "./main.js"; - -const BASE_URL = "https://panda-market-api-crud.vercel.app"; - -/** - * 아티클 리스트 조회 - * @param {Object} params { page, pageSize, keyword } - * @returns {Promise} - */ -export async function getArticleList(params = {}) { - try { - const response = await axios.get(`${BASE_URL}/articles`, { params }); - // 응답 구조가 { list: [ {...} ], totalCount: … } 라는 가정 - const articles = response.data.list.map((item) => { - // 생성일이 있을수도 있고 없을수도 있으므로 기본값 처리 - const article = new Article( - item.title, - item.content, - item.writer || "알 수 없음" - ); - return article; - }); - return articles; - } catch (error) { - console.error("getArticleList 에러:", error.message); - throw error; - } -} - -/** - * 단일 아티클 조회 - * @param {string|number} id - * @returns {Promise
} - */ -export async function getArticle(id) { - try { - const response = await axios.get(`${BASE_URL}/articles/${id}`); - const { title, content, writer } = response.data; - const article = new Article(title, content, writer || "알 수 없음"); - return article; - } catch (error) { - console.error("getArticle 에러:", error.message); - throw error; - } -} - -/** - * 아티클 생성 - * @param {Object} data { title, content, image } - * @returns {Promise} - */ -export async function createArticle(data) { - try { - // 반드시 image 필드를 배열로 - const body = { - title: data.title, - content: data.content, - image: Array.isArray(data.image) ? data.image : [data.image], - }; - const response = await axios.post(`${BASE_URL}/articles`, body); - const { title, content, writer } = response.data; - const article = new Article(title, content, writer || "알 수 없음"); - console.log("createArticle 성공:", response.status); - return article; - } catch (error) { - const status = error.response ? error.response.status : "N/A"; - console.error(`createArticle 실패 (상태 코드: ${status}):`, error.message); - return null; - } -} - -/** - * 아티클 수정 - * @param {string|number} id - * @param {Object} data { title?, content?, image? } - * @returns {Promise} - */ -export async function patchArticle(id, data) { - try { - const body = {}; - if (data.title !== undefined) body.title = data.title; - if (data.content !== undefined) body.content = data.content; - if (data.image !== undefined) { - body.image = Array.isArray(data.image) ? data.image : [data.image]; - } - const response = await axios.patch(`${BASE_URL}/articles/${id}`, body); - const { title, content, writer } = response.data; - const article = new Article(title, content, writer || "알 수 없음"); - console.log("patchArticle 성공:", response.status); - return article; - } catch (error) { - const status = error.response ? error.response.status : "N/A"; - console.error(`patchArticle 실패 (상태 코드: ${status}):`, error.message); - return null; - } -} - -/** - * 아티클 삭제 - * @param {string|number} id - * @returns {Promise} 성공하면 true - */ -export async function deleteArticle(id) { - try { - const response = await axios.delete(`${BASE_URL}/articles/${id}`); - console.log("deleteArticle 성공:", response.status); - return true; - } catch (error) { - const status = error.response ? error.response.status : "N/A"; - console.error(`deleteArticle 실패 (상태 코드: ${status}):`, error.message); - return false; - } -} - diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4fbf110a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm ci + +COPY prisma ./prisma +RUN npx prisma generate + +COPY . . + +RUN npm run build + +RUN mkdir -p /app/uploads + +EXPOSE 3000 + +CMD ["sh", "-c", "npx prisma migrate deploy && node dist/src/server.js"] diff --git a/ProductService.js b/ProductService.js deleted file mode 100644 index bf8c00bb..00000000 --- a/ProductService.js +++ /dev/null @@ -1,48 +0,0 @@ -import axios from "axios"; -import { Product, ElectronicProduct } from "./main.js"; - -const BASE_URL = "https://panda-market-api-crud.vercel.app"; - -export function validateProduct(productData) { - if (!productData) throw new Error("productData가 제공되지 않았습니다."); - const { name, description, price, tags, images } = productData; - - const missingFields = []; - if (!name) missingFields.push("name"); - if (!description) missingFields.push("description"); - if (price === undefined || price === null) missingFields.push("price"); - if (!tags) missingFields.push("tags"); - if (!images) missingFields.push("images"); - if (missingFields.length > 0) - throw new Error(`필수 필드가 누락되었습니다: ${missingFields.join(", ")}`); - - if (typeof name !== "string") throw new Error("name은 문자열이어야 합니다."); - if (typeof description !== "string") - throw new Error("description은 문자열이어야 합니다."); - if (typeof price !== "number" || price < 0) - throw new Error("price는 0 이상의 숫자여야 합니다."); - if (!Array.isArray(tags)) throw new Error("tags는 배열이어야 합니다."); - if (!Array.isArray(images)) throw new Error("images는 배열이어야 합니다."); -} - -export async function getProductList(params = {}) { - try { - const response = await axios.get(`${BASE_URL}/products`, { params }); - const products = response.data.list.map((p) => - p.tags.includes("전자제품") - ? new ElectronicProduct( - p.name, - p.description, - p.price, - p.tags, - p.images, - p.manufacturer - ) - : new Product(p.name, p.description, p.price, p.tags, p.images) - ); - return products; - } catch (error) { - console.error("상품 리스트 조회 실패:", error.message); - return []; - } -} diff --git a/app.js b/app.js deleted file mode 100644 index be38e50a..00000000 --- a/app.js +++ /dev/null @@ -1,33 +0,0 @@ -import express from "express"; -import cors from "cors"; -import dotenv from "dotenv"; -import path from "path"; -import productRouter from "./router/productrouter.js"; -import articleRouter from "./router/articlerouter.js"; -import commentRouter from "./router/commentrouter.js"; - -dotenv.config(); - -const app = express(); -const PORT = process.env.PORT || 3000; - -app.use(cors()); -app.use(express.json()); -app.use("/uploads", express.static("uploads")); -app.use(express.static("public")); -app.use((req, res, next) => { - console.log(`👉 [${req.method}] ${req.url}`); - next(); -}); -app.use("/products", productRouter); -app.use("/articles", articleRouter); -app.use("/comments", commentRouter); -app.use((req, res, next) => { - res.status(404).send({ error: "페이지를 찾을 수 없습니다 (404)" }); -}); - -app.listen(PORT, () => { - console.log(`🚀 서버 정상 가동 중: http://localhost:${PORT}`); -}); - -export default app; diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..5ee0052b --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,39 @@ +services: + app: + build: . + ports: + - "3000:3000" + environment: + DATABASE_URL: postgresql://panda:panda1234@db:5432/panda_market + JWT_ACCESS_SECRET: docker-access-secret + JWT_REFRESH_SECRET: docker-refresh-secret + JWT_ACCESS_EXPIRES_IN: 15m + JWT_REFRESH_EXPIRES_IN: 7d + PORT: "3000" + CORS_ORIGIN: http://localhost:3001 + NODE_ENV: production + volumes: + - uploads:/app/uploads + depends_on: + db: + condition: service_healthy + + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: panda + POSTGRES_PASSWORD: panda1234 + POSTGRES_DB: panda_market + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U panda -d panda_market"] + interval: 5s + timeout: 3s + retries: 5 + +volumes: + uploads: + pgdata: diff --git a/index.ts b/index.ts deleted file mode 100644 index c3be6248..00000000 --- a/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -// 알림 서버 메인 진입점 -// Express + Socket.IO를 사용한 실시간 알림 서버 - -import express from 'express'; -import http from 'http'; -import { Server as IOServer } from 'socket.io'; -import notificationRoutes from './src/routes/notification.routes'; -import { initializeSocketHandlers } from './src/socket/notification.socket'; -import { setIOInstance } from './src/controllers/notification.controller'; - -const app = express(); -const server = http.createServer(app); -const io = new IOServer(server, { - cors: { - origin: '*', - methods: ['GET', 'POST'], - }, -}); - -// 미들웨어 설정 -app.use(express.json()); -app.use(express.urlencoded({ extended: true })); - -// CORS 설정 -app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - if (req.method === 'OPTIONS') { - return res.sendStatus(200); - } - next(); -}); - -// 헬스 체크 엔드포인트 -app.get('/health', (req, res) => { - res.json({ status: 'ok', message: 'Notification server is running' }); -}); - -// 알림 API 라우트 등록 -app.use('/', notificationRoutes); - -// Socket.IO 초기화 -setIOInstance(io); -initializeSocketHandlers(io); - -// 에러 핸들링 -app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { - console.error('Unhandled error:', err); - res.status(500).json({ - success: false, - message: 'Internal server error', - }); -}); - -// 404 핸들링 -app.use((req, res) => { - res.status(404).json({ - success: false, - message: 'Route not found', - }); -}); - -const PORT = Number(process.env.PORT || 3000); - -if (require.main === module) { - server.listen(PORT, () => { - console.log(`✨ Notification server listening on port ${PORT}`); - console.log(`📡 WebSocket endpoint: http://localhost:${PORT}`); - console.log(`🏥 Health check: http://localhost:${PORT}/health`); - }); -} - -export default server; -export { io }; diff --git a/main.js b/main.js deleted file mode 100644 index 91a43b43..00000000 --- a/main.js +++ /dev/null @@ -1,88 +0,0 @@ -export class Product { - #favoriteCount; // private 필드로 캡슐화 - - constructor(name, description, price, tags = [], images = []) { - validateProduct(name, description, price, tags, images); - this.name = name; - this.description = description; - this.price = price; - this.tags = tags; - this.images = images; - this.#favoriteCount = 0; - } - - - favorite() { - this.#favoriteCount++; - } -} - -/** - * Product 데이터 검증 - * @param {Object} productData - 검증할 Product 데이터 - * @throws {Error} 검증 실패 시 에러 발생 - */ -function validateProduct(name, description, price, tags, images) { - // 필수 필드 존재 여부 확인 - const missingFields = []; - if (name === undefined) missingFields.push("name"); - if (description === undefined) missingFields.push("description"); - if (price === undefined || price === null) missingFields.push("price"); - if (!tags) missingFields.push("tags"); - if (!images) missingFields.push("images"); - - if (missingFields.length > 0) { - throw new Error(`필수 필드가 누락되었습니다: ${missingFields.join(", ")}`); - } - - // 데이터 타입 검증 - if (typeof name !== "string") { - throw new Error("name은 문자열이어야 합니다."); - } - if (typeof description !== "string") { - throw new Error("description은 문자열이어야 합니다."); - } - if (typeof price !== "number" || price < 0) { - throw new Error("price는 0 이상의 숫자여야 합니다."); - } - if (!Array.isArray(tags)) { - throw new Error("tags는 배열이어야 합니다."); - } - if (!Array.isArray(images)) { - throw new Error("images는 배열이어야 합니다."); - } -} - -// ElectronicProduct 클래스 - 상속 -export class ElectronicProduct extends Product { - constructor(name, description, price, tags = [], images = [], manufacturer) { - super(name, description, price, tags, images); - this.manufacturer = manufacturer; - } - - // 다형성: 부모 클래스의 메소드를 오버라이드할 수 있음 - favorite() { - super.favorite(); - console.log(`${this.manufacturer}의 ${this.name} 제품이 찜되었습니다.`); - } -} - -// Article 클래스 -export class Article { - #likeCount; // private 필드로 캡슐화 - #createdAt; // private 필드로 캡슐화 - - constructor(title, content, writer) { - this.title = title; - this.content = content; - this.writer = writer; - this.#likeCount = 0; - this.#createdAt = new Date(); // 현재 시간 저장 - } - - // 좋아요 메소드 - like() { - this.#likeCount++; - } -} - diff --git a/prisma/schema.js b/prisma/schema.js deleted file mode 100644 index 192a3b39..00000000 --- a/prisma/schema.js +++ /dev/null @@ -1,15 +0,0 @@ -import { createRequire } from "module"; -const require = createRequire(import.meta.url); - -const { PrismaClient } = require("../generated/prisma/index.js"); - -import "dotenv/config"; -import { PrismaPg } from "@prisma/adapter-pg"; -import pkg from "pg"; - -const connectionString = process.env.DATABASE_URL; -const pool = new pkg.Pool({ connectionString }); -const adapter = new PrismaPg(pool); -const prisma = new PrismaClient({ adapter }); - -export { prisma }; diff --git a/prisma/seed.js b/prisma/seed.js deleted file mode 100644 index aad0876f..00000000 --- a/prisma/seed.js +++ /dev/null @@ -1,59 +0,0 @@ -// prisma/seed.js -import { PrismaClient } from "@prisma/client"; // import로 변경 (이게 맞습니다) - -const prisma = new PrismaClient(); - -async function main() { - try { - // 1. 초기화 (모델 이름 소문자 필수!) - await prisma.productComment.deleteMany(); - await prisma.articleComment.deleteMany(); - await prisma.product.deleteMany(); - await prisma.article.deleteMany(); - } catch (e) { - console.log("초기화 중 오류(첫 실행이면 무시 가능):", e.message); - } - - const product1 = await prisma.product.create({ - data: { - last_name: "sprint Misson3", - description: "미션 3 어려워 상수선언 프로덕트 1 은 어웨잇 프리즈마", - price: 5959, - tags: ["sprint", "mission", "fucking", "hard"], - }, - }); - - await prisma.productComment.create({ - data: { - product_id: product1.id, - content: "진짜 미션 3 너무 어려워요ㅠㅠ", - }, - }); - - const article1 = await prisma.article.create({ - data: { - title: "아티클1 선언 프리즈마 아티클에 만들어라?", - content: "이게 뭔지 모르겠네요", - body: "바디 내용 필수라서 추가함", - }, - }); - - await prisma.articleComment.create({ - data: { - article_id: article1.id, - content: "이게 맞는듯", - }, - }); - - console.log(" 시딩 성공 ! !"); -} - -main() - .then(async () => { - await prisma.$disconnect(); - }) - .catch(async (e) => { - console.error(e); - await prisma.$disconnect(); - process.exit(1); - }); diff --git a/router/articlerouter.js b/router/articlerouter.js deleted file mode 100644 index d1a14d44..00000000 --- a/router/articlerouter.js +++ /dev/null @@ -1,99 +0,0 @@ -import express from "express"; -import { PrismaClient } from "@prisma/client"; -import multer from "multer"; -import path from "path"; - -const router = express.Router(); -const prisma = new PrismaClient(); - -const storage = multer.diskStorage({ - destination: (req, file, cb) => cb(null, "uploads/"), - filename: (req, file, cb) => { - const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); - cb(null, uniqueSuffix + path.extname(file.originalname)); - }, -}); -const upload = multer({ storage }); - -const wrap = (handler) => async (req, res, next) => { - try { - await handler(req, res, next); - } catch (e) { - console.error(e); - res.status(500).send({ message: "Server Error" }); - } -}; - -router.post("/upload", upload.single("image"), (req, res) => { - if (!req.file) return res.status(400).send({ message: "파일 없음" }); - const imagePath = req.file.path.replace(/\\/g, "/"); - res.json({ url: `http://localhost:3000/${imagePath}` }); -}); - -router.post( - "/", - wrap(async (req, res) => { - const { title, content } = req.body; - if (!title || !content) - return res.status(400).send({ message: "내용 필수" }); - - const newArticle = await prisma.article.create({ - data: { title, content }, - }); - res.status(201).send(newArticle); - }) -); - -router.get( - "/", - wrap(async (req, res) => { - const { page = 1, pageSize = 10, search = "" } = req.query; - const skip = (Number(page) - 1) * Number(pageSize); - - const articles = await prisma.article.findMany({ - where: { - OR: [ - { title: { contains: search, mode: "insensitive" } }, - { content: { contains: search, mode: "insensitive" } }, - ], - }, - - orderBy: { createdAt: "desc" }, - skip, - take: Number(pageSize), - }); - res.send(articles); - }) -); - -router.get( - "/:id", - wrap(async (req, res) => { - const article = await prisma.article.update({ - where: { id: Number(req.params.id) }, - data: { viewCount: { increment: 1 } }, - }); - res.send(article); - }) -); - -router.delete( - "/:id", - wrap(async (req, res) => { - await prisma.article.delete({ where: { id: Number(req.params.id) } }); - res.status(204).send(); - }) -); - -router.patch( - "/:id/like", - wrap(async (req, res) => { - const product = await prisma.product.update({ - where: { id: Number(req.params.id) }, - data: { likeCount: { increment: 1 } }, // 1 증가 - }); - res.send(product); // 업데이트된 정보(좋아요 수 포함) 돌려줌 - }) -); - -export default router; diff --git a/router/commentrouter.js b/router/commentrouter.js deleted file mode 100644 index cc6718d8..00000000 --- a/router/commentrouter.js +++ /dev/null @@ -1,123 +0,0 @@ -import express from "express"; -import { PrismaClient } from "@prisma/client"; -import multer from "multer"; -import path from "path"; - -const router = express.Router(); -const prisma = new PrismaClient(); - -const storage = multer.diskStorage({ - destination: (req, file, cb) => cb(null, "uploads/"), - filename: (req, file, cb) => { - const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); - cb(null, uniqueSuffix + path.extname(file.originalname)); - }, -}); -const upload = multer({ storage }); - -const wrap = (handler) => async (req, res, next) => { - try { - await handler(req, res, next); - } catch (e) { - console.error(`Error: [${req.method}] ${req.originalUrl}`); - console.error(e); - res.status(500).send({ message: "Server Error" }); - } -}; -router.post( - "/products/:productId", - upload.single("image"), - wrap(async (req, res) => { - const { productId } = req.params; - const { content } = req.body; - const imagePath = req.file ? req.file.path.replace(/\\/g, "/") : null; - - if (!content && !imagePath) - return res.status(400).send({ message: "내용/사진 필수" }); - - const comment = await prisma.productComment.create({ - data: { - content: content || "", - image: imagePath, - product_id: Number(productId), - }, - }); - res.status(201).send(comment); - }) -); -router.get( - "/products/:productId", - wrap(async (req, res) => { - const { productId } = req.params; - const { cursor, limit = 50 } = req.query; - - let options = { - where: { product_id: Number(productId) }, - take: Number(limit), - orderBy: { createdAt: "desc" }, - }; - - if (cursor) { - options.cursor = { id: Number(cursor) }; - options.skip = 1; - } - - const comments = await prisma.productComment.findMany(options); - const nextCursor = - comments.length === Number(limit) - ? comments[comments.length - 1].id - : null; - res.send({ data: comments, nextCursor }); - }) -); - -// [3] 게시글 댓글 등록 -router.post( - "/articles/:articleId", - upload.single("image"), - wrap(async (req, res) => { - const { articleId } = req.params; - const { content } = req.body; - const imagePath = req.file ? req.file.path.replace(/\\/g, "/") : null; - - if (!content && !imagePath) - return res.status(400).send({ message: "내용/사진 필수" }); - - const comment = await prisma.articleComment.create({ - data: { - content: content || "", - image: imagePath, - article_id: Number(articleId), - }, - }); - res.status(201).send(comment); - }) -); - -router.get( - "/articles/:articleId", - wrap(async (req, res) => { - const { articleId } = req.params; - const { cursor, limit = 50 } = req.query; - - let options = { - where: { article_id: Number(articleId) }, - take: Number(limit), - orderBy: { createdAt: "desc" }, - }; - - if (cursor) { - options.cursor = { id: Number(cursor) }; - options.skip = 1; - } - - const comments = await prisma.articleComment.findMany(options); - const nextCursor = - comments.length === Number(limit) - ? comments[comments.length - 1].id - : null; - res.send({ data: comments, nextCursor }); - }) -); - -export default router; diff --git a/router/productrouter.js b/router/productrouter.js deleted file mode 100644 index eef51245..00000000 --- a/router/productrouter.js +++ /dev/null @@ -1,128 +0,0 @@ -import express from "express"; -import { PrismaClient } from "@prisma/client"; -import multer from "multer"; -import path from "path"; - -const router = express.Router(); -const prisma = new PrismaClient(); - -const storage = multer.diskStorage({ - destination: (req, file, cb) => cb(null, "uploads/"), - filename: (req, file, cb) => { - const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); - cb(null, uniqueSuffix + path.extname(file.originalname)); - }, -}); -const upload = multer({ storage }); - -const wrap = (handler) => async (req, res, next) => { - try { - await handler(req, res, next); - } catch (e) { - console.error(`❌fuck Product Error: [${req.method}] ${req.originalUrl}`); - console.error(e); - res.status(500).send({ message: "Server Error" }); - } -}; - -router.get( - "/", - wrap(async (req, res) => { - const { page = 1, pageSize = 10, search = "" } = req.query; - const skip = (Number(page) - 1) * Number(pageSize); - - // 검색 조건 - const where = { - OR: [ - { last_name: { contains: search, mode: "insensitive" } }, - { description: { contains: search, mode: "insensitive" } }, - ], - }; - - const products = await prisma.product.findMany({ - where, - orderBy: { createdAt: "desc" }, - skip, - take: Number(pageSize), - }); - res.send(products); - }) -); - -router.post( - "/", - upload.single("image"), - wrap(async (req, res) => { - const { last_name, description, price, tags } = req.body; - - if (!last_name || !price) { - return res.status(400).send({ message: "상품명과 가격은 필수입니다." }); - } - - const imagePath = req.file ? req.file.path.replace(/\\/g, "/") : null; - const priceInt = Number(price); - const tagArray = tags - ? tags - .split(",") - .map((t) => t.trim()) - .filter((t) => t) - : []; - - const newProduct = await prisma.product.create({ - data: { - last_name, - description, - price: priceInt, - tags: tagArray, - image: imagePath, - }, - }); - res.status(201).send(newProduct); - }) -); - -router.get( - "/:id", - wrap(async (req, res) => { - const product = await prisma.product.update({ - where: { id: Number(req.params.id) }, - data: { viewCount: { increment: 1 } }, - }); - res.send(product); - }) -); - -router.patch( - "/:id", - wrap(async (req, res) => { - const { id } = req.params; - - if (req.body.price) req.body.price = Number(req.body.price); - - const product = await prisma.product.update({ - where: { id: Number(id) }, - data: req.body, - }); - res.send(product); - }) -); - -router.delete( - "/:id", - wrap(async (req, res) => { - await prisma.product.delete({ where: { id: Number(req.params.id) } }); - res.status(204).send(); - }) -); -router.patch( - "/:id/like", - wrap(async (req, res) => { - const product = await prisma.product.update({ - where: { id: Number(req.params.id) }, - data: { likeCount: { increment: 1 } }, // 1 증가 - }); - res.send(product); // 업데이트된 정보(좋아요 수 포함) 돌려줌 - }) -); - -export default router; diff --git a/test.js b/test.js deleted file mode 100644 index 5fc0a266..00000000 --- a/test.js +++ /dev/null @@ -1,101 +0,0 @@ -// main.js -import axios from "axios"; - -// API 기본 설정 -const API_BASE = "https://your-api-endpoint.com"; - -// 예시 상품 데이터 -const products = [ - { - id: 1, - category: "전자제품", - name: "냉장고", - price: 120000, - description: "전자제품", - tags: ["전자제품"], - image: "https://www.example.com", - }, - { - id: 2, - category: "일반제품", - name: "상품 테스트 이름", - price: 42000, - description: "string", - tags: ["전자 테스트 제품"], - image: "https://example.com/12", - }, -]; - -// 예시 아티클 데이터 -const articles = [ - { - id: 1, - title: "Axios로 [제목 수정] 완료!", - author: "알 수 없음", - content: "CRUD 테스트용", - likes: 0, - createdAt: "알 수 없음", - }, - { - id: 2, - title: "게시글 제목입니다.", - author: "알 수 없음", - content: "게시글 내용입니다.", - likes: 0, - createdAt: "알 수 없음", - }, -]; - -// 상품 리스트 출력 -function printProducts() { - console.log("==== 상품 리스트 ===="); - products.forEach((p, i) => { - console.log( - `${i + 1}. [${p.category}] ${p.name} - ${p.price.toLocaleString()}원` - ); - console.log(` 설명: ${p.description}`); - console.log(` 태그: ${p.tags.join(", ")}`); - console.log(` 이미지: ${p.image}`); - console.log("------------------------"); - }); -} - -// 아티클 리스트 출력 -function printArticles() { - console.log("==== 아티클 테스트 ===="); - articles.forEach((a, i) => { - console.log(`${i + 1}. [${a.title}] 작성자: ${a.author}`); - console.log(` 내용: ${a.content}`); - console.log(` 좋아요: ${a.likes} / 생성일: ${a.createdAt}`); - console.log("------------------------"); - }); -} - -// 아티클 생성 예시 -async function createArticle(article) { - try { - const res = await axios.post(`${API_BASE}/articles`, article); - console.log("아티클 생성 성공:", res.data); - } catch (err) { - console.error( - `createArticle 실패 (상태 코드: ${err.response?.status}):`, - err.message - ); - } -} - -// 테스트 실행 -async function runTest() { - printProducts(); - printArticles(); - - // 아티클 생성 테스트 - await createArticle({ - title: "새 테스트 글", - content: "생성 테스트", - author: "테스터", - }); -} - -runTest(); -