Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 80 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: CI
on:
push:
branches:
- feat/ci-cd # TODO: main으로 변경 필요

jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install pnpm
run: npm install -g pnpm

- name: Install dependencies
run: pnpm install

# 4. Next.js 빌드
- name: Build Next.js application
run: CI=false npm run build
env:
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}

# 5. Docker Buildx 설정
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

# 6. Docker Hub 로그인
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

# 7. Docker 이미지 빌드 및 푸시
- name: Build and push Docker image
id: docker_build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/leadme5-front:latest
build-args: |
NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}

# 8. Docker Digest 값 추출 (배포 설정에 사용할 이미지 고유 식별자)
- name: Get image digest
id: get_digest
run: echo "IMAGE_DIGEST=${{ steps.docker_build.outputs.digest }}" >> $GITHUB_ENV

# 9. 인프라 레포지토리 클론
- name: Checkout infra-k8s repository
uses: actions/checkout@v4
with:
repository: LEADME-skala5/infra-k8s
path: infra-k8s
token: ${{ secrets.GIT_PAT }}

# 10. deployment.yaml 파일 내 이미지 경로를 새 Digest로 치환
- name: Update infra-k8s deployment.yaml with new image digest
run: |
echo "Updating deployment.yaml with new image digest: ${{ env.IMAGE_DIGEST }}"
sed -i 's|image: .*/leadme5-front.*|image: ${{ secrets.DOCKERHUB_USERNAME }}/leadme5-front@${{ env.IMAGE_DIGEST }}|g' infra-k8s/front/deployment.yaml

# 11. 변경사항 커밋 및 푸시
- name: Commit and push updated deployment.yaml
run: |
cd infra-k8s
git config user.name "SanghyunLee"
git config user.email "[email protected]"
git add front/deployment.yaml
git commit -m "Update front deployment image to ${{ env.IMAGE_DIGEST }}"
git push origin main
42 changes: 42 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 1️⃣ Build Stage
FROM node:20-alpine AS builder

ARG NEXT_PUBLIC_API_URL

ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

# 작업 디렉토리
WORKDIR /app

# 필요한 파일 복사
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install

# 전체 소스 복사
COPY . .

# Next.js 빌드
RUN pnpm run build

# 2️⃣ Production Stage
FROM node:20-alpine AS runner
WORKDIR /app

ARG NEXT_PUBLIC_API_URL

RUN npm install -g pnpm

# 필요한 파일만 복사
COPY --from=builder /app/.next .next
COPY --from=builder /app/public public
COPY --from=builder /app/node_modules node_modules
COPY --from=builder /app/package.json package.json
COPY --from=builder /app/next.config.mjs next.config.mjs

# 포트 설정
EXPOSE 3000
ENV NODE_ENV=production
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

# 실행
CMD ["pnpm", "start"]
29 changes: 29 additions & 0 deletions app/login/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use server';

import { redirect } from 'next/navigation';

export async function loginAction({ userId, password }: { userId: string; password: string }) {
console.log('🔍 NEXT_PUBLIC_API_URL:', process.env.NEXT_PUBLIC_API_URL);
console.log('🔍 요청 URL:', `${process.env.NEXT_PUBLIC_API_URL}/auth/login`);

const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ employeeNumber: userId, password }),
credentials: 'include',
});

// 🔍 기본 응답 정보
console.log('🔍 Status:', res.status, res.statusText);
console.log('🔍 Headers:', Object.fromEntries(res.headers.entries()));

const data = await res.json();

if (!res.ok) {
throw new Error(data.message || '로그인 실패');
}

console.log('🔍 Success Response:', data);

redirect('/dashboard');
}
108 changes: 97 additions & 11 deletions app/team/overview/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,105 @@
import { TeamEvaluation } from '@/components/team/team-evaluation';
import { TeamReport } from '@/components/team/team-report';
import { cookies } from 'next/headers';

// Mock API call - replace with your actual API
async function getTeamEvaluationStatus() {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 100));
interface Task {
taskId: number;
name: string;
isEvaluated: boolean;
grade: number | null;
}

interface User {
userId: number;
name: string;
position: string;
email: string;
tasks: Task[];
quarterScore: number | null;
lastUpdated: string | null;
}

interface ApiResponse {
evaluated: boolean;
users: User[];
}

interface PageProps {
params: { organizationId: string };
}

// Mock data - replace with actual API response
// Return true if team has been evaluated, false if not
return Math.random() > 0.5; // Random for demonstration
function extractOrganizationIdFromToken(token: string): string | null {
try {
const payloadBase64 = token.split('.')[1];
const decodedPayload = JSON.parse(Buffer.from(payloadBase64, 'base64').toString());
return decodedPayload.organizationId?.toString() || null;
} catch (e) {
console.error('토큰 디코딩 실패:', e);
return null;
}
}

export default async function TeamOverviewPage() {
// const hasBeenEvaluated = await getTeamEvaluationStatus();
const hasBeenEvaluated = true;
async function getEvaluationData() {
try {
const cookieStore = await cookies();
const accessToken = cookieStore.get('accessToken')?.value;

if (!accessToken) throw new Error('accessToken 누락');

const organizationId = extractOrganizationIdFromToken(accessToken);
if (!organizationId) throw new Error('organizationId 추출 실패');

const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/quantitative-evaluation/${organizationId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
cache: 'no-store',
credentials: 'include',
}
);
console.log('accessToken', accessToken);

if (!res.ok) throw new Error('API 요청 실패');
return await res.json();
} catch (error) {
console.error(error);
return { evaluated: false, users: [] };
}
}

export default async function Page() {
const { evaluated, users } = await getEvaluationData();

// API 응답 → 컴포넌트 데이터 형식 변환
const teamMembers = users.map(
(user: {
userId: { toString: () => any };
name: any;
position: any;
email: any;
tasks: any[];
quarterScore: any;
lastUpdated: any;
}) => ({
id: user.userId.toString(),
name: user.name,
role: user.position,
email: user.email,
projects: user.tasks.map((task) => task.name),
performanceScore: user.quarterScore || 0, // null 대체값
lastEvaluationDate: user.lastUpdated || '', // null 대체값
})
);

return <div className="p-6">{hasBeenEvaluated ? <TeamReport /> : <TeamEvaluation />}</div>;
return (
<div className="p-6">
{evaluated ? (
<TeamReport teamMembers={teamMembers} />
) : (
<TeamEvaluation teamMembers={teamMembers} />
)}
</div>
);
}
46 changes: 25 additions & 21 deletions components/auth/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import Link from 'next/link';
import { useUserStore } from '@/store/useUserStore';

export function LoginForm() {
const setUser = useUserStore((state) => state.setUser);
const router = useRouter();
const setUser = useUserStore((state) => state.setUser);
const [formData, setFormData] = useState({
userId: '',
password: '',
Expand Down Expand Up @@ -50,33 +50,37 @@ export function LoginForm() {
setErrors({});

try {
const res = await fetch('/api/login', {
// 클라이언트 컴포넌트에서 직접 API 호출
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: formData.userId,
password: formData.password,
}),
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ employeeNumber: formData.userId, password: formData.password }),
credentials: 'include', // 쿠키를 자동으로 저장하기 위해 필요
});

const data = await res.json();

if (!res.ok) {
setErrors({ general: data.error || '로그인 실패' });
return;
// 응답 검사
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || '로그인 실패');
}

const user = data.user;
const accessToken = data.accessToken;
// 성공 시 사용자 정보 저장
const userData = await response.json();

// Zustand 스토어에 사용자 정보 저장
// setUser({
// id: userData.id || userData.userId,
// name: userData.name,
// role: userData.role || userData.position,
// // 기타 필요한 사용자 정보
// });

setUser(user, accessToken);
localStorage.setItem('user', JSON.stringify(user));
// 대시보드로 리다이렉트
router.push('/dashboard');
} catch (error) {
setErrors({ general: '서버 연결 오류' });
router.refresh(); // 페이지 갱신 (선택적)
} catch (error: any) {
setErrors({ general: error.message || '로그인 실패' });
console.error('로그인 오류:', error);
} finally {
setIsLoading(false);
}
Expand Down
Loading