Skip to content
This repository was archived by the owner on Jan 2, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9936d9e
๐Ÿ“ฆ Chore: ๋นŒ๋“œ ์„ค์ • ์ถ”๊ฐ€
thelightway24 Apr 28, 2025
1127be7
๐Ÿ“ฆ Chore: ๋นŒ๋“œ ํ…Œ์ŠคํŠธ
thelightway24 Apr 28, 2025
d1c8a18
๐Ÿ“ฆ Chore: ๋นŒ๋“œ ํ…Œ์ŠคํŠธ#1
thelightway24 Apr 28, 2025
89c2110
๐Ÿ“ฆ Chore: ๋„์ปค ํŒŒ์ผ ์ถ”๊ฐ€ (#2)
thelightway24 Apr 28, 2025
89934c9
๐Ÿ› Bug: ๋ˆ„๋ฝ๋œ ํŽ˜์ด์ง€ push
HaechangLee Apr 28, 2025
9401659
๐Ÿ› Bug: ๋ˆ„๋ฝ๋œ ๋ฉ”์ธ ํŽ˜์ด์ง€ Push
HaechangLee Apr 28, 2025
643ebb4
โœจ Feature: ์ฆ์ƒ๊ฒ€์ƒ‰ ๋ฐฑ์—”๋“œ ์—ฐ๋™ (#4)
HaechangLee Apr 29, 2025
7ac9dfd
โœจ Feat: ์ž์—ฐ์–ด ๊ฒ€์ƒ‰ ๋ฐฑ์—”๋“œ ์—ฐ๋™
HaechangLee Apr 29, 2025
c4610df
๐Ÿ’„ Style: ๊ฒ€์ƒ‰๊ฒฐ๊ณผ ์ปจํ…Œ์ด๋„ˆ ๋””์ž์ธ ๊ฐœ์„  (#8)
HaechangLee Apr 30, 2025
ec12ad0
โœจ Feat: ์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ํŽ˜์ด์ง€ ์—ฐ๋™ (#10)
HaechangLee May 1, 2025
3d06f65
Revert "โœจ Feat: ์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ํŽ˜์ด์ง€ ์—ฐ๋™ (#10)" (#11)
thelightway24 May 1, 2025
dbc3706
โœจ Feature/#9 ์•ฝํ’ˆ ์ƒ์„ธ์ •๋ณด ํŽ˜์ด์ง€ (#12)
thelightway24 May 1, 2025
37e855c
โ™ป๏ธ Refactor: ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ตฌํ˜„ ๋ฐ ๊ฒ€์ƒ‰์ฐฝ ๊ฐœ์„ 
HaechangLee May 3, 2025
933afb8
๐Ÿ› Fix: ๊ฒ€์ƒ‰ ์—”๋“œํฌ์ธํŠธ ์—ฐ๋™
HaechangLee May 4, 2025
2252d9d
โœจ Feat: ์„ฑ๋ถ„๋ช… ๊ฒ€์ƒ‰ ๋ฐฑ์—”๋“œ ์—ฐ๋™ (#23)
HaechangLee May 5, 2025
2aa6640
Update docker-compose.yml
thelightway24 May 5, 2025
3ed54ed
๐Ÿ› Bug: ์„ฑ๋ถ„๊ฒ€์ƒ‰ ์—”๋“œํฌ์ธํŠธ ์ˆ˜์ • (#25)
HaechangLee May 5, 2025
53d7deb
โœจ Feat: ์„ฑ๋ถ„๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๋ผ์šฐํŒ… ์ •์ƒํ™” (#27)
HaechangLee May 5, 2025
346cb86
๐Ÿ› Fix: ์ž์—ฐ์–ด๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‘œ์ถœ๋ฐฉ์‹ ์ˆ˜์ • ๋ฐ ๋ชจ๋ฐ”์ผ ํŽ˜์ด์ง€ ๋Œ€์‘ (#29)
HaechangLee May 6, 2025
4331fbf
๐Ÿ’„ Style: placeholder ์ƒ‰์ƒ ์ˆ˜์ •
HaechangLee May 6, 2025
6814f4c
๐Ÿ’„ Style: ๊ฒ€์ƒ‰์–ด ์ƒ‰์ƒ ์ˆ˜์ •
HaechangLee May 6, 2025
c71ba34
โ™ป๏ธ Refactor: ์ดˆ๊ธฐ ๊ฒ€์ƒ‰ํ™”๋ฉด ์ž์—ฐ์–ด๋กœ ์ˆ˜์ •
thelightway24 May 20, 2025
ea613ba
๐Ÿ’„ Style: ๊ฒ€์ƒ‰๋ชจ๋“œ ์ •๋ ฌ์ˆœ์„œ ๋ณ€๊ฒฝ (#36)
HaechangLee May 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/dev-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Frontend Deploy

on:
push:
branches:
- dev
paths-ignore:
- '.github/workflows/**'

jobs:
deploy:
runs-on: self-hosted

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Copy frontend code to shared volume
run: |
rm -rf /deploy/app/*
cp -r . /deploy/app/

- name: Restart Docker (Frontend Node.js)
run: |
cd /deploy/app
docker-compose down
docker-compose up -d --build
33 changes: 33 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 1) Builder ๋‹จ๊ณ„: ์˜์กด ์„ค์น˜ + ๋นŒ๋“œ
FROM node:18-alpine AS builder
WORKDIR /app

# package-lock.json๊นŒ์ง€ ๋ณต์‚ฌํ•ด์„œ ์ •ํ™•ํžˆ ์„ค์น˜
COPY package.json package-lock.json ./
RUN npm ci

# ์†Œ์Šค ์ „์ฒด ๋ณต์‚ฌ
COPY . .

# Next.js ๋นŒ๋“œ (App Router ๊ธฐ์ค€์œผ๋กœ .next + static ํŽ˜์ด์ง€ ์ƒ์„ฑ)
RUN npm run build

# 2) Production ๋‹จ๊ณ„: ๋Ÿฐํƒ€์ž„์šฉ์œผ๋กœ ๊ฒฝ๋Ÿ‰ํ™”
FROM node:18-alpine
WORKDIR /app

# production ํ™˜๊ฒฝ ๋ณ€์ˆ˜
ENV NODE_ENV=production

# ๋นŒ๋“œ ๊ฒฐ๊ณผ๋ฌผ๋งŒ ๋ณต์‚ฌ
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/package-lock.json ./package-lock.json
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/public ./public

# ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์—ด ํฌํŠธ
EXPOSE 3000

# next start ๋กœ ์„œ๋ฒ„ ๊ตฌ๋™
CMD ["npm", "start"]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Yak+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started
Expand Down
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: '3.8'

services:
next-app:
build:
context: .
dockerfile: Dockerfile
container_name: yakplus-frontend
environment:
- NODE_ENV=production
ports:
- "13000:3000"
restart: unless-stopped
networks:
- deploy_yakplus

networks:
deploy_yakplus:
external: true
1 change: 1 addition & 0 deletions public/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
256 changes: 256 additions & 0 deletions src/app/drugs/[id]/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
'use client';

import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import NoImage from '@/components/NoImage';
import Link from 'next/link';

export default function DrugDetailPage() {
const params = useParams();
const drugId = params.id;

const [drug, setDrug] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {

const fetchDrugDetail = async () => {
try {
setLoading(true);
const response = await fetch(`/api/drugs/search/detail/${drugId}`);
if (!response.ok) {
throw new Error('์•ฝํ’ˆ ์ •๋ณด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.');
}

const data = await response.json();
setDrug(data.data);
} catch (err) {
console.error('์•ฝํ’ˆ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ ์˜ค๋ฅ˜:', err);
setError(err.message);
} finally {
setLoading(false);
}
};

fetchDrugDetail();
}, [drugId]);

if (loading) {
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1 flex items-center justify-center">
<div className="text-center py-16">๋กœ๋”ฉ ์ค‘...</div>
</main>
<Footer />
</div>
);
}

if (error || !drug) {
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1 flex items-center justify-center">
<div className="text-center py-16 text-red-500">
{error || '์•ฝํ’ˆ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'}
</div>
</main>
<Footer />
</div>
);
}

// ์•ฝํ’ˆ ์ „๋ฌธ/์ผ๋ฐ˜ ๊ตฌ๋ถ„
const getEtcOtcName = (isGeneral) => {
return isGeneral ? "์ผ๋ฐ˜์˜์•ฝํ’ˆ" : "์ „๋ฌธ์˜์•ฝํ’ˆ";
};

// ์ฃผ์˜์‚ฌํ•ญ ํ‚ค ๊ฐ’ ์ถ”์ถœ
const precautionKeys = drug.precaution ? Object.keys(drug.precaution) : [];

return (
<div className="min-h-screen flex flex-col bg-gray-50">
<Header />
<main className="flex-1 w-full mt-[64px]">
<div className="max-w-5xl mx-auto w-full px-4 py-8">
<div className="bg-white rounded-lg shadow-md p-6">
{/* ์ƒ๋‹จ ์˜์—ญ: ์•ฝํ’ˆ ๊ธฐ๋ณธ ์ •๋ณด */}
<div className="flex flex-col md:flex-row gap-8 mb-8">
{/* ์•ฝํ’ˆ ์ด๋ฏธ์ง€ */}
<div className="w-full md:w-80 h-80 flex-shrink-0">
{drug.imageUrl ? (
<img
src={drug.imageUrl}
alt={drug.drugName}
className="w-full h-full rounded-lg object-contain border border-gray-200"
/>
) : (
<NoImage className="w-full h-full" />
)}
</div>

{/* ์•ฝํ’ˆ ๊ธฐ๋ณธ ์ •๋ณด */}
<div className="flex-1 space-y-4">
<h1 className="text-2xl font-bold text-gray-800">{drug.drugName}</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex items-center">
<span className="w-24 text-sm font-medium text-gray-500">์ œ์•ฝํšŒ์‚ฌ</span>
<span className="text-gray-700">{drug.company}</span>
</div>
<div className="flex items-center">
<span className="w-24 text-sm font-medium text-gray-500">ํ’ˆ๋ชฉ๊ธฐ์ค€์ฝ”๋“œ</span>
<span className="text-gray-700">{drug.drugId}</span>
</div>
<div className="flex items-center">
<span className="w-24 text-sm font-medium text-gray-500">๋ณด๊ด€๋ฐฉ๋ฒ•</span>
<span className="text-gray-700">{drug.storeMethod}</span>
</div>
<div className="flex items-center">
<span className="w-24 text-sm font-medium text-gray-500">์˜์•ฝํ’ˆ ๊ตฌ๋ถ„</span>
<span className="text-gray-700">{getEtcOtcName(drug.isGeneral)}</span>
</div>
</div>
<div className="space-y-2">

<div className="flex items-center">
<span className="w-24 text-sm font-medium text-gray-500">ํ—ˆ๊ฐ€์ผ</span>
<span className="text-gray-700">{drug.permitDate}</span>
</div>
<div className="flex items-center">
<span className="w-24 text-sm font-medium text-gray-500">์œ ํšจ๊ธฐ๊ฐ„</span>
<span className="text-gray-700">
{drug.validTerm ? drug.validTerm : '์ •๋ณด ์—†์Œ'}
</span>
</div>
<div className="flex items-center">
<span className="w-24 text-sm font-medium text-gray-500">์ทจ์†Œ์ผ์ž</span>
<span className="text-gray-700">
{drug.cancelDate ? drug.cancelDate : 'ํ•ด๋‹น ์—†์Œ'}
</span>
</div>
<div className="flex items-center">
<span className="w-24 text-sm font-medium text-gray-500">์ทจ์†Œ์‚ฌ์œ </span>
<span className="text-gray-700">
{drug.cancelName ? drug.cancelName : 'ํ•ด๋‹น ์—†์Œ'}
</span>
</div>
</div>
</div>
</div>
</div>

{/* ์„ฑ๋ถ„ ์ •๋ณด */}
{drug.materialInfo && drug.materialInfo.length > 0 && (
<div className="mb-6">
<h2 className="text-lg font-bold px-4 py-2 rounded-md mb-3 border-b border-gray-300 pb-2">
์„ฑ๋ถ„ ์ •๋ณด
</h2>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th className="px-4 py-2 bg-gray-50 text-left text-sm font-medium text-gray-500">์„ฑ๋ถ„๋ช…</th>
<th className="px-4 py-2 bg-gray-50 text-left text-sm font-medium text-gray-500">๋ถ„๋Ÿ‰</th>
<th className="px-4 py-2 bg-gray-50 text-left text-sm font-medium text-gray-500">๋‹จ์œ„</th>
<th className="px-4 py-2 bg-gray-50 text-left text-sm font-medium text-gray-500">์ด๋Ÿ‰</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{drug.materialInfo.map((material, index) => (
<tr key={index}>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">
<Link
href={`/search/ingredient?q=${encodeURIComponent(material.์„ฑ๋ถ„๋ช…)}&mode=keyword&type=ingredient`}
className="text-blue-600 hover:text-blue-800 hover:underline"
>
{material.์„ฑ๋ถ„๋ช…}
</Link>
</td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{material.๋ถ„๋Ÿ‰}</td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{material.๋‹จ์œ„}</td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{material.์ด๋Ÿ‰}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}

{/* ์ƒ์„ธ ์ •๋ณด ์„น์…˜ */}
<div className="border-t pt-6 space-y-6">
{/* ํšจ๋Šฅํšจ๊ณผ */}
<div className="space-y-3">
<h2 className="text-lg font-bold px-4 py-2 text-[#2BA89C] rounded-md border-b border-gray-300 pb-2">
ํšจ๋Šฅํšจ๊ณผ
</h2>
<div className="px-4 py-2 text-gray-700">
{drug.efficacy?.length > 0 ? (
<ul className="list-none pl-0 space-y-1">
{drug.efficacy.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
) : (
<p>์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>
)}
</div>
</div>

{/* ์šฉ๋ฒ•์šฉ๋Ÿ‰ */}
<div className="space-y-3">
<h2 className="text-lg font-bold px-4 py-2 text-[#2BA89C] rounded-md border-b border-gray-300 pb-2">
์šฉ๋ฒ•์šฉ๋Ÿ‰
</h2>
<div className="px-4 py-2 text-gray-700">
{drug.usage?.length > 0 ? (
<ul className="list-none pl-0 space-y-1">
{drug.usage.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
) : (
<p>์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>
)}
</div>
</div>

{/* ์ฃผ์˜์‚ฌํ•ญ ๋ฐ ๊ธฐํƒ€ ์ •๋ณด */}
{precautionKeys.length > 0 && (
<div className="space-y-3">
<h2 className="text-lg font-bold px-4 py-2 text-[#2BA89C] rounded-md border-b border-gray-300 pb-2">
์ฃผ์˜์‚ฌํ•ญ
</h2>
{precautionKeys.map((key) => (
<div key={key} className="space-y-3">
<h3 className="text-md font-semibold px-4 py-2 text-gray-700">
{key}
</h3>
<div className="px-4 py-2 text-gray-700">
{drug.precaution[key]?.length > 0 ? (
<ul className="list-none pl-0 space-y-1">
{drug.precaution[key].map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
) : (
<p>์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.</p>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</main>
<Footer />
</div>
);
}
7 changes: 5 additions & 2 deletions src/app/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ const geistMono = Geist_Mono({
});

export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "์•ฝํ’ˆ ๊ฒ€์ƒ‰ ํ”Œ๋žซํผ Yak+",
description: "์•ฝํ’ˆ ๊ฒ€์ƒ‰ ํ”Œ๋žซํผ Yak+",
icons: {
icon: '/logo.svg', // SVG ๊ฒฝ๋กœ
},
};

export default function RootLayout({ children }) {
Expand Down
Loading