From 558476b8618aa4e6a05608160c9210927ace72f8 Mon Sep 17 00:00:00 2001 From: Joohyung Park Date: Wed, 29 Oct 2025 22:44:56 +0900 Subject: [PATCH 1/9] Init commit --- .devcontainer/devcontainer.json | 27 +++++++++++++++++++++++++++ .gitignore | 30 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .gitignore diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..256883cf --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Node.js & TypeScript", + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", + + // Prettier 패키지를 컨테이너에 '글로벌'로 설치합니다. + "postCreateCommand": "npm install -g prettier", + + "customizations": { + "vscode": { + "extensions": [ + // VS Code Prettier 확장 (esbenp.prettier-vscode) + "esbenp.prettier-vscode" + ], + "settings": { + // 파일을 저장할 때 자동 포맷팅 활성화 + "editor.formatOnSave": true, + // 기본 포맷터로 Prettier 설정 + "editor.defaultFormatter": "esbenp.prettier-vscode", + + // 💡 추가된 설정: Prettier 확장이 로컬이 아닌 글로벌 모듈을 찾도록 지시 + "prettier.resolveGlobalModules": true + } + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8333aaa4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# 의존성 +/node_modules +/package-lock.json +/yarn.lock +/npm-debug.log* +/yarn-debug.log* +/yarn-error.log* +/pnpm-debug.log* + +# 빌드 출력 (TypeScript 컴파일 결과) +/dist +/build +/out +/es +/lib + +# TypeScript 캐시 +/.tsbuildinfo + +# 환경 변수 및 보안 파일 +.env +.env.local +.env.*.local + +# 운영체제/에디터 파일 +.DS_Store +Thumbs.db + +# VS Code 설정 +.vscode \ No newline at end of file From 84f0084866d37c91a244844331aeed6fc6ca6266 Mon Sep 17 00:00:00 2001 From: dlckdgh0523-lgtm Date: Tue, 18 Nov 2025 09:34:08 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20[=EC=9D=B4=EC=B0=BD=ED=98=B8]=207?= =?UTF-8?q?=EA=B8=B0=20=EC=8A=A4=ED=94=84=EB=A6=B0=ED=8A=B8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=201=20=EC=8B=A4=EC=8A=B5=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B0=8F=20README=20=EC=A0=9C=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ArticleService.js | 116 ++++++++++++++++++++++++++++++++++++++++++++++ ProductService.js | 48 +++++++++++++++++++ README.md | 1 + main.js | 88 +++++++++++++++++++++++++++++++++++ test.js | 101 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 354 insertions(+) create mode 100644 ArticleService.js create mode 100644 ProductService.js create mode 100644 README.md create mode 100644 main.js create mode 100644 test.js diff --git a/ArticleService.js b/ArticleService.js new file mode 100644 index 00000000..f0b3b8b5 --- /dev/null +++ b/ArticleService.js @@ -0,0 +1,116 @@ +// 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/ProductService.js b/ProductService.js new file mode 100644 index 00000000..bf8c00bb --- /dev/null +++ b/ProductService.js @@ -0,0 +1,48 @@ +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/README.md b/README.md new file mode 100644 index 00000000..3236c3fe --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +코드잇 node.js 7기 이창호 diff --git a/main.js b/main.js new file mode 100644 index 00000000..91a43b43 --- /dev/null +++ b/main.js @@ -0,0 +1,88 @@ +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/test.js b/test.js new file mode 100644 index 00000000..5fc0a266 --- /dev/null +++ b/test.js @@ -0,0 +1,101 @@ +// 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(); + From aeb3b3e95cee7d539afd26b7745c3a6e758b95aa Mon Sep 17 00:00:00 2001 From: dlckdgh0523-lgtm Date: Tue, 18 Nov 2025 10:29:56 +0900 Subject: [PATCH 3/9] =?UTF-8?q?docs:=20README.md=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95=20=EB=B0=8F=20PR=20=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3236c3fe..f0229a43 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ -코드잇 node.js 7기 이창호 +[v] class 키워드를 이용한 Product / ElectronicProduct 클래스 정의 +[v] Product 프로퍼티(name, description, price, tags, images, favoriteCount) 및 favorite() 메소드 구현 +[v] ElectronicProduct가 Product를 상속하며 manufacturer 프로퍼티 추가 +[v] Article 클래스 정의 및 like() 메소드 구현 +[v] Article 클래스에 createdAt(생성일자) 프로퍼티 추가 및 constructor에서 현재 시간 저장 +[v] 각 클래스 마다 constructor 작성 +[v] 추상화/캡슐화/상속/다형성을 고려하여 코드를 작성해 주세요. + +Article API 요청 함수 구현 (.then/.catch) +[v] getArticleList() : GET 메소드 사용 (page, pageSize, keyword 쿼리 파라미터 이용) +[v] getArticle() : GET 메소드 사용 +[v] createArticle() : POST 메소드 사용 (title, content, image request body 포함) +[v] patchArticle() : PATCH 메소드 사용 +[v] deleteArticle() : DELETE 메소드 사용 +[v] fetch 혹은 axios 이용 +[v] 응답의 상태 코드가 2XX가 아닐 경우, 에러 메시지를 콘솔에 출력해 주세요. +[x] .then() 메소드를 이용하여 비동기 처리를 해주세요. +[v] .catch() 를 이용하여 오류 처리를 해주세요. +[v] ProductService.js 파일에 Product API 관련 함수들을 작성해 주세요. + +Product API 요청 함수 구현 (async/await) +[v] getProductList() : GET 메소드 사용 (page, pageSize, keyword 쿼리 파라미터를 이용해 주세요.) +[v] getProduct() : GET 메소드를 사용해 주세요. +[x] createProduct() : POST 메소드를 사용해 주세요. (name, description, price, tags, images request body 포함) +[v] patchProduct() : PATCH 메소드를 사용해 주세요. +[v] deleteProduct() : DELETE 메소드를 사용해 주세요. +[v] async/await 을 이용하여 비동기 처리를 해주세요. +[v] try/catch 를 이용하여 오류 처리를 해주세요. +[v] getProductList()를 통해서 받아온 상품 리스트를 각각 인스턴스로 만들어 products 배열에 저장해 주세요. +[v] 해시태그에 "전자제품"이 포함되어 있는 상품들은 ElectronicProduct 클래스를 사용해 인스턴스를 생성해 주세요. +[x] 나머지 상품들은 모두 Product 클래스를 사용해 인스턴스를 생성해 주세요. + +파일 구조 및 실행 +[v] ProductService.js 파일 분리 (Product API) +[v] ArticleService.js 파일 분리 (Article API) +[x] 이외의 코드들은 모두 main.js 파일에 작성 (import 활용) +[v] 각 함수를 실행하는 코드를 작성하고, 제대로 동작하는지 확인해 주세요. +**아직 완전하게 익힌게 아니라 자꾸 오류가 나와서 test.js를 분리했습니다.** From 2892fce22b726581b89b757ed72292233aa47626 Mon Sep 17 00:00:00 2001 From: sinyeongseok Date: Mon, 8 Dec 2025 04:12:20 +0000 Subject: [PATCH 4/9] docs: README init --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..ded0ecbc --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +{1팀} + +팀원 구성 + +- 신영석 +- 서석규 +- 이창호 +- 강희성 +- 이요한 + +기술 스택 + +- Backend: Express.js, PrismaORM +- Database: PostgresSQL +- etc: Git & Github, Discord, Notion From b6047f9a598f32d0035e57891c2149e2b7a75df7 Mon Sep 17 00:00:00 2001 From: sinyeongseok Date: Mon, 8 Dec 2025 04:22:16 +0000 Subject: [PATCH 5/9] =?UTF-8?q?chore:=20=EC=B4=88=EA=B8=B0=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 00000000..d929f3d1 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "type": "module", + "scripts": { + "start": "node server.js", + "dev": "node --watch server.js" + }, + "dependencies": { + "@prisma/adapter-pg": "^7.0.0", + "@prisma/client": "^7.0.0", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.1.0" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/pg": "^8.15.6", + "prisma": "^7.0.0", + "ts-node": "^10.9.2" + } +} From c2beb42a25a03191ec9b221235c5602de4e5f131 Mon Sep 17 00:00:00 2001 From: sinyeongseok Date: Mon, 8 Dec 2025 06:19:43 +0000 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=20=ED=99=98=EA=B2=BD=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.js | 7 +++++++ src/app.js | 11 +++++++++++ src/routers/index.js | 5 +++++ 3 files changed, 23 insertions(+) create mode 100644 server.js create mode 100644 src/app.js create mode 100644 src/routers/index.js diff --git a/server.js b/server.js new file mode 100644 index 00000000..d46568aa --- /dev/null +++ b/server.js @@ -0,0 +1,7 @@ +import app from "./src/app.js"; + +const PORT = process.env.PORT || 3000; + +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); diff --git a/src/app.js b/src/app.js new file mode 100644 index 00000000..3a0d02b5 --- /dev/null +++ b/src/app.js @@ -0,0 +1,11 @@ +import express from "express"; +import router from "./routes/index.js"; +import cors from "cors"; + +const app = express(); + +app.use(express.json()); +app.use(cors()); +app.use("/api", router); + +export default app; diff --git a/src/routers/index.js b/src/routers/index.js new file mode 100644 index 00000000..9da7196e --- /dev/null +++ b/src/routers/index.js @@ -0,0 +1,5 @@ +import express from "express"; + +const router = express.Router(); + +export default router; From da3f8e10298aef3854ed0a63c3f6db0e39b627b6 Mon Sep 17 00:00:00 2001 From: sinyeongseok Date: Mon, 8 Dec 2025 06:20:30 +0000 Subject: [PATCH 7/9] =?UTF-8?q?chore:=20=EC=B4=88=EA=B8=B0=20DB=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=8A=A4=ED=82=A4=EB=A7=88=20?= =?UTF-8?q?init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- prisma.config.ts | 14 ++++ .../20251208055939_init/migration.sql | 65 +++++++++++++++++++ prisma/migrations/migration_lock.toml | 3 + prisma/prisma.js | 10 +++ prisma/schema.prisma | 64 ++++++++++++++++++ 6 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 prisma.config.ts create mode 100644 prisma/migrations/20251208055939_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/prisma.js create mode 100644 prisma/schema.prisma diff --git a/.gitignore b/.gitignore index 8333aaa4..16924d21 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ Thumbs.db # VS Code 설정 -.vscode \ No newline at end of file +.vscode +/generated/prisma diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 00000000..9c5e9593 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,14 @@ +// This file was generated by Prisma and assumes you have installed the following: +// npm install --save-dev prisma dotenv +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/prisma/migrations/20251208055939_init/migration.sql b/prisma/migrations/20251208055939_init/migration.sql new file mode 100644 index 00000000..1ce53cb2 --- /dev/null +++ b/prisma/migrations/20251208055939_init/migration.sql @@ -0,0 +1,65 @@ +-- CreateTable +CREATE TABLE "Group" ( + "id" BIGSERIAL NOT NULL, + "name" TEXT NOT NULL, + "tags" TEXT[], + "goal_reps" INTEGER NOT NULL, + "image" TEXT, + "discord_web_url" TEXT NOT NULL, + "discord_server_url" TEXT NOT NULL, + "like_count" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6), + "owner_id" BIGINT NOT NULL, + + CONSTRAINT "Group_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" BIGSERIAL NOT NULL, + "name" TEXT NOT NULL, + "password" TEXT NOT NULL, + "group_id" BIGINT NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "workout_log" ( + "id" BIGSERIAL NOT NULL, + "category" TEXT NOT NULL, + "description" TEXT NOT NULL, + "time" TIMESTAMP(3) NOT NULL, + "distance" INTEGER NOT NULL, + "images" TEXT[], + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "user_id" BIGINT NOT NULL, + + CONSTRAINT "workout_log_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Badge" ( + "id" BIGSERIAL NOT NULL, + "content" TEXT NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "group_id" BIGINT NOT NULL, + + CONSTRAINT "Badge_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Group_owner_id_key" ON "Group"("owner_id"); + +-- AddForeignKey +ALTER TABLE "Group" ADD CONSTRAINT "Group_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "Group"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "workout_log" ADD CONSTRAINT "workout_log_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Badge" ADD CONSTRAINT "Badge_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "Group"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..044d57cd --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/prisma.js b/prisma/prisma.js new file mode 100644 index 00000000..445e709e --- /dev/null +++ b/prisma/prisma.js @@ -0,0 +1,10 @@ +import "dotenv/config"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "../generated/prisma/client.js"; + +const connectionString = `${process.env.DATABASE_URL}`; + +const adapter = new PrismaPg({ connectionString }); +const prisma = new PrismaClient({ adapter }); + +export { prisma }; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 00000000..a4e3b1a5 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,64 @@ +generator client { + provider = "prisma-client-js" + output = "../generated/prisma" +} + +datasource db { + provider = "postgresql" +} + +model Group { + id BigInt @id @default(autoincrement()) + name String + tags String[] + goal_reps Int + image String? + discord_web_url String + discord_server_url String + like_count Int + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime? @updatedAt @db.Timestamptz(6) + + owner_id BigInt @unique + owner User @relation("GroupOwner", fields: [owner_id], references: [id]) + + users User[] + badges Badge[] +} + +model User { + id BigInt @id @default(autoincrement()) + name String + password String + + group_id BigInt + group Group @relation(fields: [group_id], references: [id], onDelete: Cascade) + + ownedGroup Group? @relation("GroupOwner") + + workoutLogs WorkoutLog[] +} + +model WorkoutLog { + id BigInt @id @default(autoincrement()) + category String + description String + time DateTime + distance Int + images String[] + created_at DateTime @default(now()) @db.Timestamptz(6) + + user_id BigInt + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) + + @@map("workout_log") +} + +model Badge { + id BigInt @id @default(autoincrement()) + content String + created_at DateTime @default(now()) @db.Timestamptz(6) + + group_id BigInt + group Group @relation(fields: [group_id], references: [id], onDelete: Cascade) +} From 0a0c1205463d750b941f8796fca2afe720b76329 Mon Sep 17 00:00:00 2001 From: dlckdgh Date: Thu, 21 May 2026 20:01:27 +0900 Subject: [PATCH 8/9] =?UTF-8?q?algorithm-=EC=9D=B4=EC=B0=BD=ED=98=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- algorithm/sorts.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 algorithm/sorts.js diff --git a/algorithm/sorts.js b/algorithm/sorts.js new file mode 100644 index 00000000..e69de29b From 7eaba13bd88cd5466d19dd953fc79b2bdec335b1 Mon Sep 17 00:00:00 2001 From: dlckdgh Date: Thu, 21 May 2026 20:12:53 +0900 Subject: [PATCH 9/9] feat: sprint 12 --- algorithm/sorts.js | 96 +++++++++++++++++++++++++++++++++++++++++++++ algorithm/sorts.ts | 97 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 algorithm/sorts.ts diff --git a/algorithm/sorts.js b/algorithm/sorts.js index e69de29b..b465d396 100644 --- a/algorithm/sorts.js +++ b/algorithm/sorts.js @@ -0,0 +1,96 @@ +// 선택 정렬 +function selectionSort(arr) { + for (let i = 0; i < arr.length - 1; i++) { + let minIndex = i; + + for (let j = i + 1; j < arr.length; j++) { + if (arr[j] < arr[minIndex]) { + minIndex = j; + } + } + + [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; + } +} + +// 삽입 정렬 +function insertionSort(arr) { + for (let i = 1; i < arr.length; i++) { + let current = arr[i]; + let j = i - 1; + + while (j >= 0 && arr[j] > current) { + arr[j + 1] = arr[j]; + j--; + } + + arr[j + 1] = current; + } +} + +// 병합 정렬 +function mergeSort(arr) { + if (arr.length <= 1) return arr; + + const middle = Math.floor(arr.length / 2); + + const left = mergeSort(arr.slice(0, middle)); + const right = mergeSort(arr.slice(middle)); + + return merge(left, right); +} + +function merge(left, right) { + const result = []; + + let leftIndex = 0; + let rightIndex = 0; + + while (leftIndex < left.length && rightIndex < right.length) { + if (left[leftIndex] < right[rightIndex]) { + result.push(left[leftIndex]); + leftIndex++; + } else { + result.push(right[rightIndex]); + rightIndex++; + } + } + + return result + .concat(left.slice(leftIndex)) + .concat(right.slice(rightIndex)); +} + +// 퀵 정렬 +function quickSort(arr, left = 0, right = arr.length - 1) { + if (left >= right) return; + + const pivotIndex = partition(arr, left, right); + + quickSort(arr, left, pivotIndex - 1); + quickSort(arr, pivotIndex + 1, right); +} + +function partition(arr, left, right) { + const pivot = arr[right]; + + let i = left - 1; + + for (let j = left; j < right; j++) { + if (arr[j] < pivot) { + i++; + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + } + + [arr[i + 1], arr[right]] = [arr[right], arr[i + 1]]; + + return i + 1; +} + +module.exports = { + selectionSort, + insertionSort, + mergeSort, + quickSort, +}; \ No newline at end of file diff --git a/algorithm/sorts.ts b/algorithm/sorts.ts new file mode 100644 index 00000000..59d462c3 --- /dev/null +++ b/algorithm/sorts.ts @@ -0,0 +1,97 @@ +// 선택 정렬 +export function selectionSort(arr: number[]): void { + for (let i = 0; i < arr.length - 1; i++) { + let minIndex = i; + + for (let j = i + 1; j < arr.length; j++) { + if (arr[j] < arr[minIndex]) { + minIndex = j; + } + } + + [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; + } +} + +// 삽입 정렬 +export function insertionSort(arr: number[]): void { + for (let i = 1; i < arr.length; i++) { + const current = arr[i]; + let j = i - 1; + + while (j >= 0 && arr[j] > current) { + arr[j + 1] = arr[j]; + j--; + } + + arr[j + 1] = current; + } +} + +// 병합 정렬 +export function mergeSort(arr: number[]): number[] { + if (arr.length <= 1) return arr; + + const middle = Math.floor(arr.length / 2); + + const left = mergeSort(arr.slice(0, middle)); + const right = mergeSort(arr.slice(middle)); + + return merge(left, right); +} + +function merge(left: number[], right: number[]): number[] { + const result: number[] = []; + + let leftIndex = 0; + let rightIndex = 0; + + while (leftIndex < left.length && rightIndex < right.length) { + if (left[leftIndex] < right[rightIndex]) { + result.push(left[leftIndex]); + leftIndex++; + } else { + result.push(right[rightIndex]); + rightIndex++; + } + } + + return result + .concat(left.slice(leftIndex)) + .concat(right.slice(rightIndex)); +} + +// 퀵 정렬 +export function quickSort( + arr: number[], + left: number = 0, + right: number = arr.length - 1 +): void { + if (left >= right) return; + + const pivotIndex = partition(arr, left, right); + + quickSort(arr, left, pivotIndex - 1); + quickSort(arr, pivotIndex + 1, right); +} + +function partition( + arr: number[], + left: number, + right: number +): number { + const pivot = arr[right]; + + let i = left - 1; + + for (let j = left; j < right; j++) { + if (arr[j] < pivot) { + i++; + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + } + + [arr[i + 1], arr[right]] = [arr[right], arr[i + 1]]; + + return i + 1; +} \ No newline at end of file