Skip to content

Commit 4d6bc8c

Browse files
committed
직무추천페이지_직무로드맵페이지
1 parent d713023 commit 4d6bc8c

9 files changed

Lines changed: 1501 additions & 250 deletions

File tree

package-lock.json

Lines changed: 261 additions & 188 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"@dnd-kit/core": "^6.3.1",
13+
"@dnd-kit/sortable": "^10.0.0",
14+
"@dnd-kit/utilities": "^3.2.2",
1215
"@hookform/resolvers": "^5.2.1",
1316
"axios": "^1.11.0",
1417
"clsx": "^2.1.1",
15-
"next": "^15.3.5",
16-
"react": "^19.0.0",
17-
"react-dom": "^19.0.0",
18+
"next": "^15.5.0",
19+
"openai": "^5.13.1",
20+
"react": "^19.1.1",
21+
"react-dom": "^19.1.1",
1822
"react-hook-form": "^7.62.0",
1923
"tailwind-merge": "^3.3.1",
2024
"zod": "^4.0.17",
@@ -25,6 +29,7 @@
2529
"@types/axios": "^0.9.36",
2630
"@types/node": "^20.19.11",
2731
"@types/react": "^19",
32+
"@types/react-beautiful-dnd": "^13.1.8",
2833
"@types/react-dom": "^19",
2934
"autoprefixer": "^10.4.21",
3035
"eslint": "^9",

src/pages/api/chat.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// pages/api/chat.ts
2+
3+
import type { NextApiRequest, NextApiResponse } from 'next';
4+
import OpenAI from 'openai';
5+
6+
const openai = new OpenAI({
7+
apiKey: process.env.OPENAI_API_KEY,
8+
});
9+
10+
type ResponseData = {
11+
answer?: string;
12+
error?: string;
13+
};
14+
15+
export default async function handler(
16+
req: NextApiRequest,
17+
res: NextApiResponse<ResponseData>
18+
) {
19+
if (req.method !== 'POST') {
20+
return res.status(405).json({ error: 'Method Not Allowed' });
21+
}
22+
23+
try {
24+
const { prompt } = req.body;
25+
26+
if (!prompt) {
27+
return res.status(400).json({ error: 'Prompt is required' });
28+
}
29+
30+
const response = await openai.chat.completions.create({
31+
model: 'gpt-3.5-turbo',
32+
messages: [{ role: 'user', content: prompt }],
33+
temperature: 0.7,
34+
});
35+
36+
const answer = response.choices[0].message.content;
37+
res.status(200).json({ answer });
38+
39+
} catch (error) {
40+
console.error('API Route Error:', error);
41+
res.status(500).json({ error: 'Failed to fetch response from OpenAI' });
42+
}
43+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// pages/job-recommendations/final_result.tsx
2+
3+
'use client';
4+
5+
import React, { useMemo } from 'react';
6+
import { Layout } from '@/layout/Layout';
7+
import { useRouter, useSearchParams } from 'next/navigation';
8+
9+
// ... (직무 데이터, 가중치 등 모든 상단 코드는 그대로 유지)
10+
const jobDetails: { [key: string]: { title: string; description: string } } = {
11+
'ai-engineer': { title: 'AI 엔지니어', description: '사람처럼 생각하고 학습하는 인공지능(AI) 모델과 서비스를 만듭니다. 챗봇, 상품 추천 시스템, 음성 비서 등 AI 기술을 실제 문제 해결에 적용하는 역할을 합니다.' },
12+
'digital-marketer': { title: '디지털 마케터', description: '소셜 미디어, 검색엔진, 유튜브 등 온라인 채널을 통해 제품이나 서비스를 홍보합니다. 데이터를 분석하여 가장 효과적인 방법으로 사람들에게 다가가는 전략을 세웁니다.' },
13+
'software-engineer': { title: '소프트웨어 엔지니어', description: '컴퓨터 프로그램, 스마트폰 앱, 웹사이트 등 우리가 사용하는 대부분의 소프트웨어를 설계하고 개발하며 테스트하는 전 과정을 책임지는 개발자입니다.' },
14+
'backend-developer': { title: '백엔드 개발자', description: '웹사이트나 앱의 보이지 않는 뒷부분(서버, 데이터베이스)을 만듭니다. 사용자가 눈으로 보는 기능들이 원활하게 작동하도록 데이터를 처리하고 관리하는 역할을 합니다.' },
15+
'frontend-developer': { title: '프론트엔드 개발자', description: '웹사이트나 앱에서 사용자에게 직접 보이는 화면(UI)을 만들고 디자인합니다. 사용자가 편리하게 서비스를 이용할 수 있도록 버튼, 레이아웃 등을 구현합니다.' },
16+
'cybersecurity-engineer': { title: '사이버 보안 엔지니어', description: '해킹이나 바이러스 같은 외부의 위협으로부터 회사의 컴퓨터 시스템과 데이터를 지키는 정보 보안 전문가입니다. 취약점을 분석하고 방어 시스템을 구축합니다.' },
17+
'data-engineer': { title: '데이터 엔지니어', description: '방대한 데이터가 잘 흐를 수 있도록 길(파이프라인)을 만들고, 데이터를 저장할 효율적인 창고(시스템)를 구축하고 관리하는 역할을 합니다.' },
18+
'cloud-architect': { title: '클라우드 솔루션 아키텍트', description: '기업이 클라우드 컴퓨팅을 효과적으로 사용할 수 있도록 전체적인 시스템을 설계하고 최적의 기술 전략을 제시하는 전문가입니다.' },
19+
'data-scientist': { title: '데이터 과학자', description: '데이터 속에서 의미 있는 패턴과 인사이트를 찾아내는 ‘데이터 탐정’입니다. 통계와 머신러닝 기술을 이용해 미래를 예측하고 비즈니스 문제 해결을 돕습니다.' },
20+
'iot-architect': { title: 'IoT 아키텍트', description: '스마트홈 기기나 스마트 팩토리처럼, 여러 사물(Things)을 인터넷으로 연결하는 전체 시스템을 설계합니다. 센서, 네트워크, 데이터 처리까지 모든 과정을 계획합니다.' },
21+
'robotics-engineer': { title: '로봇 엔지니어', description: '공장의 조립 로봇부터 수술용 로봇까지 다양한 로봇을 연구하고 설계, 제작합니다. 기계, 전자, 소프트웨어 기술을 모두 활용하여 로봇이 원하는 작업을 수행하게 만듭니다.' },
22+
'autonomous-vehicle-engineer': { title: '자율주행차 엔지니어', description: '자동차나 드론이 사람의 조작 없이 스스로 주변 환경을 인식하고 판단하여 주행할 수 있게 만드는 핵심 기술을 개발합니다.' },
23+
'computer-vision-engineer': { title: '컴퓨터 비전 엔지니어', description: '컴퓨터에 사람의 \'눈\'을 만들어주는 전문가입니다. 카메라로 들어온 이미지나 영상을 컴퓨터가 이해하고 분석하여 얼굴 인식, 사물 구별 등을 할 수 있게 하는 기술을 개발합니다.' },
24+
};
25+
const JOB_ORDER = ['ai-engineer', 'frontend-developer', 'backend-developer', 'software-engineer', 'data-engineer', 'cybersecurity-engineer', 'data-scientist', 'cloud-architect', 'iot-architect', 'robotics-engineer', 'autonomous-vehicle-engineer', 'computer-vision-engineer', 'digital-marketer'];
26+
const surveyWeights: { [key: number]: number[] } = {
27+
1: [0.2, 0.1, 0.25, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.1],
28+
2: [-0.1, 0.15, 0.1, -0.2, 0.25, -0.25, -0.25, -0.2, -0.15, -0.15, -0.2, -0.2, 0.15],
29+
3: [0.1, -0.25, 0.15, 0.25, -0.25, 0.1, 0.2, 0.2, 0, 0.1, 0, -0.1, -0.1],
30+
4: [0, -0.1, 0.1, 0.15, -0.1, 0.1, 0.2, 0.25, -0.1, 0.25, 0.15, 0.15, 0],
31+
5: [0, -0.2, 0, 0.1, -0.25, -0.1, 0.25, 0.15, -0.15, 0.1, -0.1, -0.1, -0.1],
32+
6: [0.15, 0.2, -0.1, -0.1, -0.2, -0.1, -0.1, -0.15, 0.25, -0.1, -0.1, 0, 0.1],
33+
7: [-0.1, -0.25, 0, 0.1, -0.2, 0.25, 0.05, 0.15, -0.2, 0, -0.2, -0.15, -0.2],
34+
8: [0, -0.15, 0, 0.1, -0.1, 0, 0.1, 0.1, -0.15, 0.25, 0.2, 0.1, 0],
35+
9: [0.1, -0.25, 0.05, -0.1, -0.25, -0.2, -0.15, -0.1, -0.1, 0.2, 0.25, 0.2, 0.1],
36+
10: [0.2, -0.15, 0, -0.15, -0.15, -0.2, -0.15, -0.15, 0.15, 0, 0.1, 0.2, 0.25],
37+
11: [0.25, -0.1, 0.1, 0, -0.2, -0.1, 0, -0.1, 0.2, 0.1, 0.15, 0.2, 0.2],
38+
12: [0.2, -0.25, 0, -0.1, -0.25, -0.15, -0.1, -0.1, 0.1, 0.1, 0.2, 0.25, 0.25],
39+
};
40+
const skillJobMapping: { [key: string]: string[] } = {
41+
'python': ['ai-engineer', 'data-scientist', 'backend-developer', 'data-engineer'],
42+
'data-analysis': ['data-scientist', 'digital-marketer', 'data-engineer', 'ai-engineer'],
43+
'machine-learning': ['ai-engineer', 'data-scientist', 'computer-vision-engineer'],
44+
'cloud-computing': ['cloud-architect', 'backend-developer', 'data-engineer', 'devops'],
45+
'digital-marketing': ['digital-marketer'],
46+
'leadership': ['project-management', 'team-management', 'cloud-architect'],
47+
'deep-learning': ['ai-engineer', 'data-scientist', 'autonomous-vehicle-engineer', 'computer-vision-engineer'],
48+
'project-management': ['project-management', 'software-engineer'],
49+
'nlp': ['ai-engineer', 'data-scientist'],
50+
'computer-vision': ['computer-vision-engineer', 'ai-engineer', 'autonomous-vehicle-engineer'],
51+
'cpp': ['robotics-engineer', 'autonomous-vehicle-engineer', 'software-engineer'],
52+
'team-management': ['project-management', 'software-engineer'],
53+
'java': ['backend-developer', 'software-engineer'],
54+
'tensorflow': ['ai-engineer', 'data-scientist', 'deep-learning'],
55+
'social-media-marketing': ['digital-marketer'],
56+
'agile': ['software-engineer', 'frontend-developer', 'backend-developer', 'project-management'],
57+
'devops': ['backend-developer', 'cloud-architect', 'software-engineer'],
58+
'sql': ['data-scientist', 'data-engineer', 'backend-developer', 'digital-marketer'],
59+
'communication': Object.keys(jobDetails), // 모든 직무에 중요
60+
'r': ['data-scientist', 'data-analysis'],
61+
};
62+
63+
64+
interface RecommendedJob {
65+
id: string;
66+
title: string;
67+
description: string;
68+
score: number;
69+
matchRate: number;
70+
}
71+
72+
export default function FinalResultsPage() {
73+
const router = useRouter();
74+
const searchParams = useSearchParams();
75+
76+
// ▼▼▼ 오타 수정 ▼▼▼
77+
const recommendedJobs = useMemo(() => {
78+
const surveyAnswersParam = searchParams.get('answers');
79+
const selectedSkillsParam = searchParams.get('skills');
80+
81+
if (!surveyAnswersParam || !selectedSkillsParam) return [];
82+
83+
let surveyAnswers, selectedSkills;
84+
try {
85+
surveyAnswers = JSON.parse(surveyAnswersParam);
86+
selectedSkills = JSON.parse(selectedSkillsParam);
87+
} catch (error) {
88+
console.error("URL 파라미터 분석 중 오류 발생:", error);
89+
return [];
90+
}
91+
92+
const scores: { [key: string]: number } = JOB_ORDER.reduce((acc, job) => ({ ...acc, [job]: 0 }), {});
93+
for (const questionId in surveyAnswers) {
94+
const userAnswer = surveyAnswers[questionId];
95+
const weights = surveyWeights[parseInt(questionId, 10)];
96+
if (weights) {
97+
weights.forEach((weight, index) => {
98+
const jobId = JOB_ORDER[index];
99+
scores[jobId] += userAnswer * weight;
100+
});
101+
}
102+
}
103+
const SKILL_BONUS = 1.5;
104+
selectedSkills.forEach((skillId: string) => {
105+
const relatedJobs = skillJobMapping[skillId];
106+
if (relatedJobs) {
107+
relatedJobs.forEach(jobId => {
108+
if (scores[jobId] !== undefined) {
109+
scores[jobId] += SKILL_BONUS;
110+
}
111+
});
112+
}
113+
});
114+
const sortedJobs = JOB_ORDER.map(id => ({ id, score: scores[id] }))
115+
.sort((a, b) => b.score - a.score);
116+
117+
const MAX_PERCENTAGE = 85;
118+
const MIN_PERCENTAGE = 40;
119+
const topScore = sortedJobs[0]?.score > 0 ? sortedJobs[0].score : 1;
120+
121+
const finalJobs = sortedJobs.slice(0, 3).map((job) => {
122+
const calculatedRate = MIN_PERCENTAGE + (MAX_PERCENTAGE - MIN_PERCENTAGE) * (job.score / topScore);
123+
const matchRate = Math.round(job.score > 0 ? calculatedRate : MIN_PERCENTAGE);
124+
125+
return {
126+
...job,
127+
title: jobDetails[job.id].title,
128+
description: jobDetails[job.id].description,
129+
matchRate,
130+
};
131+
});
132+
133+
return finalJobs;
134+
}, [searchParams]);
135+
136+
const handleGoToRoadmap = () => {
137+
if (recommendedJobs.length === 0) return;
138+
const jobsQueryParam = encodeURIComponent(JSON.stringify(recommendedJobs));
139+
router.push(`/job-roadmaps?jobs=${jobsQueryParam}`);
140+
};
141+
142+
return (
143+
<Layout>
144+
<div className="bg-white min-h-screen py-20">
145+
<div className="w-full max-w-5xl mx-auto px-4 text-center">
146+
<header className="mb-12">
147+
<h1 className="text-4xl font-bold text-gray-800">당신에게 추천하는 직무</h1>
148+
<p className="mt-4 text-lg text-gray-600">추가적으로 궁금한 직무가 있다면 카드를 클릭해보세요.</p>
149+
</header>
150+
151+
<main className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16">
152+
{recommendedJobs.length > 0 ? recommendedJobs.map(job => (
153+
<div key={job.id} className="bg-white border border-gray-200 rounded-xl p-8 text-left shadow-sm hover:shadow-lg hover:border-blue-500 transition-all duration-300 cursor-pointer">
154+
<h2 className="text-xl font-bold text-gray-900">{job.title}</h2>
155+
<p className="text-lg font-bold text-orange-500 my-2">{job.matchRate}% 일치</p>
156+
<p className="text-gray-600 text-sm leading-relaxed">{job.description}</p>
157+
</div>
158+
)) : (
159+
<p>추천 직무를 계산 중입니다...</p>
160+
)}
161+
</main>
162+
163+
<footer className="text-center">
164+
<button
165+
onClick={handleGoToRoadmap}
166+
className="bg-blue-600 text-white font-bold py-4 px-10 rounded-lg text-lg hover:bg-blue-700 transition-colors duration-200"
167+
>
168+
직무 로드맵 제공받으러 가기
169+
</button>
170+
</footer>
171+
</div>
172+
</div>
173+
</Layout>
174+
);
175+
}

0 commit comments

Comments
 (0)