From 6dd7f3ce75a13fbcfc2ac5d8e01c05c0670f02e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= Date: Tue, 12 May 2026 19:21:30 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Dockerfile=20=EB=B0=8F=20GitHub=20A?= =?UTF-8?q?ctions=20CI/CD=20=EC=B6=94=EA=B0=80=20(ECR=20=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - next.config.ts: output standalone 모드 설정 - Dockerfile: Node.js 20 Alpine 멀티스테이지 빌드 - .github/workflows/ci.yml: 타입검사/린트/빌드 + main 푸시 시 ECR 자동 배포 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 68 ++++++++++++++++++++++++++++++++++++++++ Dockerfile | 33 +++++++++++++++++++ next.config.ts | 2 +- 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 Dockerfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a06cb1b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI/CD + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Type check + run: npx tsc --noEmit + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build + env: + NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api' }} + NEXT_PUBLIC_SSE_URL: ${{ secrets.NEXT_PUBLIC_SSE_URL || 'http://localhost:8080/api/stream' }} + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, tag, and push image to ECR + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: dgu-cap-frontend + IMAGE_TAG: ${{ github.sha }} + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG + docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest + echo "Image pushed: $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c190737 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM node:20-alpine AS base + +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/next.config.ts b/next.config.ts index e9ffa30..68a6c64 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", }; export default nextConfig; From 81fce54b0c615fd654dcc8c4cd45d4f61cc7c87f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=84=9C=EC=A4=80?= Date: Tue, 12 May 2026 19:28:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20lint=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(useEffect=20setState,=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20import=20=EC=A0=9C=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- app/page.tsx | 1 - app/pods/page.tsx | 2 +- app/tickets/[id]/page.tsx | 10 ++++------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index b7f4f64..2e39de5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,7 +4,6 @@ import { useQuery } from "@tanstack/react-query"; import { getPods, getTickets } from "./lib/api"; import { useAlertStore } from "./store/useAlertStore"; import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; -import { Badge } from "../components/ui/badge"; import { Table, TableBody, diff --git a/app/pods/page.tsx b/app/pods/page.tsx index b0a57cb..dd79e3c 100644 --- a/app/pods/page.tsx +++ b/app/pods/page.tsx @@ -12,7 +12,7 @@ import { TableHeader, TableRow, } from "../../components/ui/table"; -import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card"; +import { Card, CardContent } from "../../components/ui/card"; import { Dialog, DialogContent, diff --git a/app/tickets/[id]/page.tsx b/app/tickets/[id]/page.tsx index 8c8b3cb..f4fc040 100644 --- a/app/tickets/[id]/page.tsx +++ b/app/tickets/[id]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import type { Ticket } from "../../lib/types"; import { useParams } from "next/navigation"; @@ -49,7 +49,7 @@ export default function TicketDetailPage() { const id = params.id as string; const queryClient = useQueryClient(); - const [status, setStatus] = useState("OPEN"); + const [statusOverride, setStatusOverride] = useState(null); const [action, setAction] = useState(""); const [memo, setMemo] = useState(""); const [performedBy, setPerformedBy] = useState(""); @@ -60,9 +60,7 @@ export default function TicketDetailPage() { queryFn: () => getTicket(id), }); - useEffect(() => { - if (ticket) setStatus(ticket.status); - }, [ticket]); + const status = statusOverride ?? ticket?.status ?? "OPEN"; const { data: logs } = useQuery({ queryKey: ["ticket-logs", id], @@ -216,7 +214,7 @@ export default function TicketDetailPage() {