diff --git a/2025-PNU-final-report.pdf b/2025-PNU-final-report.pdf new file mode 100644 index 00000000..95c93020 Binary files /dev/null and b/2025-PNU-final-report.pdf differ diff --git a/2025-PNU-mid-report.pdf b/2025-PNU-mid-report.pdf new file mode 100644 index 00000000..95c93020 Binary files /dev/null and b/2025-PNU-mid-report.pdf differ diff --git a/README.md b/README.md index 2f8e72c3..59b11980 100644 --- a/README.md +++ b/README.md @@ -1,249 +1,177 @@ -# Template for Study Group -이 레파지토리는 참여자들이 학습공동체 결과물을 위한 레파지토리 생성시에 참고할 내용들을 담고 있습니다. -1. 레파지토리 생성 -2. 레파지토리 구성 -3. README.md 가이드라인 -4. README.md 작성팁 -
- - -## 1. 레파지토리 생성 - -- https://classroom.github.com/a/wUrpZB4m -- 위 Github Classroom 링크에 접속해 본인 조의 github 레파지토리를 생성하세요. - classroom에서 팀 생성 그림 -- 레파지토리 생성 시 팀 이름은 `{조번호}` 형식으로 생성하세요. -- 예를 들어, 3조의 팀명은 `03` 입니다. -- 이 경우 `PNUSW-03`이라는 이름으로 레포지토리가 생성됩니다. -- 팀원의 경우 생성되어 있는 팀에 참가해주세요.
- 팀에 참가하지 않았을 경우, 레포지토리에 대한 권한이 없어 PR 및 commit이 막힐 수 있습니다. - classroom에서 팀 참여 그림 -
- - -## 2. 레파지토리 구성 -- 레파지토리 내에 `README.md` 파일 생성하고 아래의 가이드라인과 작성팁을 참고하여 파일을 작성하세요. -- 레파지토리 내에 `docs` 폴더를 생성하고 폴더 내에는 과제 수행 하면서 작성한 각종 보고서, 발표자료를 올려둡니다. -- 그 밖에 레파지토리의 폴더 구성은 과제 결과물에 따라 자유롭게 구성하되 가급적 코드의 목적이나 기능에 따라 폴더를 나누어 구성하세요. -
- - -## 3. README.md 가이드라인 -- README 파일 작성시에 아래의 5가지 항목의 내용은 필수적으로 포함해야 합니다. -- 아래의 7가지 항목이외에 프로젝트의 이해를 돕기위한 내용을 추가해도 됩니다. -- `SAMPLE_README.md`가 단순한 형태의 예제이니 참고하세요. -```markdown ### 1. 프로젝트 소개 #### 1.1. 개발배경 및 필요성 -> 프로젝트를 실행하게 된 배경 및 필요성을 작성하세요. +이 프로젝트는 창의융합 해커톤에 참여하는 사람들에게 README 작성의 가이드라인을 제공하기 위해 제작되었습니다. +
-#### 1.2. 개발 목표 및 주요 내용 -> 프로젝트의 목표 및 주요 내용을 작성하세요. +#### 1.2. 개발목표 및 주요내용 +창의융합 해커톤을 참여하는 사람들의 README 작성방법 이해을 돕는 것입니다. +
#### 1.3. 세부내용 -> 위 내용을 작성하세요. +가이드라인은 README에 들어가야 할 목차와 대략적인 내용을 설명합니다. +
-#### 1.4. 기존 서비스 대비 차별성 -> 위 내용을 작성하세요. +#### 1.4. 기존 서비스(상품) 대비 차별성 +> 작성하세요. +
-#### 1.5. 사회적가치 도입 계획 -> 위 내용을 작성하세요. +#### 1.5. 사회적가지 도입 계획 +> 작성하세요. +
-### 2. 상세설계 +### 2.상세설계 #### 2.1. 시스템 구성도 -> 시스템 구성도(infra, front, back등의 node 간의 관계)의 사진을 삽입하세요. +시스템 구성도 +
+ +#### 2.3. 사용기술 +| 이름 | 버전 | +|:---------------------:|:-------:| +| Python | 3.8.0 | +| Django | 3.2.9 | +| Django Rest Framework | 3.12.0 | +| Node.js | 16.16.0 | +| Vue.js | 2.5.13 | +
-#### 2.1. 사용 기술 -> 스택 별(backend, frontend, designer등) 사용한 기술 및 버전을 작성하세요. -> -> ex) React.Js - React14, Node.js - v20.0.2 ### 3. 개발결과 +[코딩역량강화플랫폼 Online Judge](http://10.125.121.115:8080/)를 예시로 작성하였습니다. #### 3.1. 전체시스템 흐름도 -> 위 내용을 작성하세요. +- 유저 플로우 차트 + > 코딩 역량강화 플랫폼의 회원가입 부분만 작성했습니다.
+ > 사용자의 행동 흐름을 도식화하여 보여줍니다. + 유저 플로우 차트 + +- 테스크 플로우 차트 + > 코딩 역량강화 플랫폼의 로그인 부분만 작성했습니다.
+ > 주요 테스크의 프로세스를 도식화하여 보여줍니다. + 테스크 플로우 차트 + +- 시스템 플로우 차트 + > 코딩 역량강화 플랫폼의 로그인 부분만 작성했습니다.
+ > 테스크의 흐름에 따른 데이터 처리를 도식화하여 보여줍니다. + 시스템 플로우 차트 + + +- IA(Information Architecture) + > 정보나 시스템의 구조를 도식화하여 보여줍니다.
+ IA -#### 3.2. 기능설명 -> 각 페이지 마다 사용자의 입력의 종류와 입력에 따른 결과 설명 및 시연 영상. -> -> ex. 로그인 페이지: -> -> - 이메일 주소와 비밀번호를 입력하면 입력창에서 유효성 검사가 진행됩니다. -> -> - 요효성 검사를 통과하지 못한 경우, 각 경고 문구가 입력창 하단에 표시됩니다. -> -> - 유효성 검사를 통과한 경우, 로그인 버튼이 활성화 됩니다. -> -> - 로그인 버튼을 클릭 시, 입력한 이메일 주소와 비밀번호에 대한 계정이 있는지 확인합니다. -> -> - 계정이 없는 경우, 경고문구가 나타납니다. -> -> (영상) - -#### 3.3. 기능명세서 -> 개발한 제품에 대한 기능명세서를 작성해 제출하세요. -> -> 노션 링크, 한글 문서, pdf 파일, 구글 스프레드 시트 등... - -#### 3.4. 디렉토리 구조 -> 위 레포지토리의 디렉토리 구조를 설명하세요. - -### 4. 설치 및 사용 방법 -> 제품을 설치하기 위헤 필요한 소프트웨어 및 설치 방법을 작성하세요. -> -> 제품을 설치하고 난 후, 실행 할 수 있는 방법을 작성하세요. - -### 5. 소개 및 시연 영상 -> 프로젝트에 대한 소개와 시연 영상을 넣으세요. -> 프로젝트 소개 동영상을 교육원 메일(swedu@pusan.ac.kr)로 제출 이후 센터에서 부여받은 youtube URL주소를 넣으세요. - -### 6. 팀 소개 -> 팀원 소개 & 구성원 별 역할 분담 & 간단한 연락처를 작성하세요. - -### 7. 해커톤 참여 후기 -> 팀원 별 해커톤 참여 후기를 작성하세요. -```
+#### 3.2. 기능설명 +##### ` 메인 페이지 ` +- 상단 배너 + - 3초에 마다 자동으로 내용이 넘어갑니다.
+ ![상단 배너](https://github.com/pnuswedu/SW-Hackathon-2024/assets/34933690/4640389f-dcaf-4b78-916e-188c8e9c6ee7) + +- 공지사항 + - 최근 5개의 공지사항을 보여줍니다. + - 발행된지 일주일이 안 된 공지사항은 new라는 mark표시를 해줍니다. + - 공지사항 글을 클릭하면 해당 공지사항 게시글로 이동합니다. + - 상단의 더보기 버튼을 클릭하면 공지사항 페이지로 이동합니다.
+ 공지사항 + +- 이번 주 보너스 문제 + - 이번 주의 보너스 점수를 주는 문제를 보여줍니다. + - 문제를 클릭하면, 해당 문제의 게시글로 이동합니다.
+ 이번 주 보너스 문제 + +- 실시간 랭킹 + - 상위 랭킹 10명의 유저를 보여줍니다. + - 상단의 더보기 버튼을 클릭하면 전체 랭킹 페이지로 이동합니다.
+ 실시간 랭킹 +
-## 4. README.md 작성 팁 -- 마크다운 언어를 이용해 README.md 파일을 작성할 때 참고할 수 있는 마크다운 언어 문법을 공유합니다. -- 다양한 예제와 보다 자세한 문법은 [이 문서](https://www.markdownguide.org/basic-syntax/)를 참고하세요. - -### 4.1. 헤더 Header -``` -# This is a Header 1 -## This is a Header 2 -### This is a Header 3 -#### This is a Header 4 -##### This is a Header 5 -###### This is a Header 6 -####### This is a Header 7 은 지원되지 않습니다. -``` - -# This is a Header 1 -## This is a Header 2 -### This is a Header 3 -#### This is a Header 4 -##### This is a Header 5 -###### This is a Header 6 -####### This is a Header 7 은 지원되지 않습니다. -
- -### 4.2. 인용문 BlockQuote -``` -> This is a first blockqute. -> > This is a second blockqute. -> > > This is a third blockqute. -``` -> This is a first blockqute. -> > This is a second blockqute. -> > > This is a third blockqute. -
- -### 4.3. 목록 List -* **Ordered List** -``` -1. first -2. second -3. third -``` -1. first -2. second -3. third -
- -* **Unordered List** -``` -* 하나 - * 둘 - -+ 하나 - + 둘 +##### ` 문제 페이지 ` +- 문제 목록 + - 사용자가 설정한 한 번에 보여줄 문제 갯수 만큼 한 화면에 문제를 띄워줍니다. + - 검색창에서 문제의 제목 및 번호로 문제를 검색할 수 있습니다. + - 난이도, 영역, 카테고리 별로 문제를 볼 수 있습니다. + - 상단의 shuffle 이모지를 클릭하면 랜덤으로 선택된 문제 푸는 페이지로 이동합니다. + - 목록에서 문제를 클릭하면 해당 문제를 푸는 페이지로 이동합니다. + ![문제 목록](https://github.com/pnuswedu/SW-Hackathon-2024/assets/34933690/95afd0db-b5a7-4628-ac9c-164513a9e51b) +
-- 하나 - - 둘 -``` -* 하나 - * 둘 -+ 하나 - + 둘 +#### 3.3. 기능명세서 +실시간 랭킹 + +|라벨|이름|상세| +|:---:|:----------------------------:|:---| +| S1 | 부산대학교 웹메일 | - 부산대 웹메일 형식인지 검증
- 중복되는 이메일인지 검증 | +| S2 | 부산대학교 웹메일 인증 코드 전송| - 클릭 시 인증 코드 메일로 전송 | +| S3 | 메일 인증 코드 | - 인증 요청 버튼 클릭 후 활성화
- 유효시간 5분| +| S4 | 메일 인증 코드 확인 | - 인증코드 검증 | +| S5 | 닉네임 | - 4 ~ 12자 영어, 숫자, '_' 가능 | +| S6 | 단과대학 선택 | -부산대학교 단과대학 리스트 보여주기 | +| S7 | 학과 선택 | - 단과대학 안의 학과 리스트 보여주기 | +| S8 | 비밀번호 | - 입력 시 텍스트 보이지 않도록 •로 표현해주기
- 6자 이상 20자 이하, 영어와 숫자 조합 필수 | +| S9 | 비밀번호 확인 | - 입력 시 텍스트 보이지 않도록 •로 표현해주기
- 비밀번호와 동일한 지 검증 | +| S10 | 회원가입 완료 | - 비어 있는 입력 칸이 없는지 검증
-메일 인증 완료했는지 확인
-조건을 만족하면 회원가입 성공| +| S11 | 로그인 | - 클릭 시 로그인 모달로 전환 | -- 하나 - - 둘 -
+
-### 4.4. 코드 CodeBlock -* 코드 블럭 이용 '``' +#### 3.4. 디렉토리 구조 ``` -여러줄 주석 "```" 이용 -"``` -#include -int main(void){ - printf("Hello world!"); - return 0; -} -```" - -단어 주석 "`" 이용 -"`Hello world`" - -* 큰 따움표(") 없이 사용하세요. -``` -
- -### 4.5. 링크 Link +├── build/ # webpack 설정 파일 +├── config/ # 프로젝트 설정 파일 +├── deplay/ # 배포 설정 파일 +├── src/ # 소스 코드 +│ ├── assets/ # 이미지, 폰트 등의 정적 파일 +│ ├── pages/ # 화면에 나타나는 페이지 +│ │ ├── page1/ # 페이지1 +│ │ ├── page2/ # 페이지2 +│ │ ├── components/ # 여러 페이지에서 공통적으로 사용되는 컴포넌트 +│ ├── router/ # 라우터 +│ ├── store/ # global state store +│ ├── styles/ # 스타일 +│ ├── utils/ # 유틸리티 +├── static/ # 정적 파일 ``` -[Title](link) -[부산대 소프트웨어융합교육원](https://swedu.pusan.ac.kr/swedu/index.do) - - - -``` -[부산대 소프트웨어융합교육원](https://swedu.pusan.ac.kr) +
- -
-### 4.6. 강조 Highlighting -``` -*single asterisks* -_single underscores_ -**double asterisks** -__double underscores__ -~~cancelline~~ -``` -*single asterisks*
-_single underscores_
-**double asterisks**
-__double underscores__
-~~cancelline~~
-
- -### 4.7. 이미지 Image -``` -Alt text -![Alt text](/path/to/img.jpg "Optional title") +### 4. 설치 및 사용 방법 +**필요 패키지** +- 위의 사용 기술 참고 + +```bash +$ git clone https://github.com/test/test.git +$ cd test/frontend +$ npm i +$ export NODE_ENV="development" # windows: set NODE_ENV=development +$ npm run build:dll +$ export TARGET="http://localhost:8000" # windows: set NODE_ENV=http://localhost:8000 +$ npm run dev ``` -부산대학교 소프트웨어융합교육원
-![부산대학교 소프트웨어융합교육원](https://github.com/pnuswedu/SW-Hackathon-2024/assets/34933690/884154bb-28f6-4498-9f64-a8a878972951, "부산대학교 소프트웨어융합교육원") -
- - - - - - - - - +### 5. 소개 및 시연영상 +[소개 및 시연영상](https://www.youtube.com/watch?v=EfEgTrm5_u4) +
+### 6. 팀 소개 +| MEMBER1 | MEMBER2 | MEMBER3 | +|:-------:|:-------:|:-------:| +|MEMBER1 | MEMBER2 | MEMBER3 | +| member1@pusan.ac.kr | member2@gmail.com | member3@naver.com | +| 프론트앤드 개발 | 인프라 구축
백앤드 개발 | DB 설계
백앤드 개발 | +
+### 7. 해커톤 참여 후기 +- MEMBER1 + > 작성하세요. +- MEMBER2 + > 작성하세요. +- MEMBER3 + > 작성하세요. +
diff --git a/SAMPLE_README1.md b/SAMPLE_README1.md deleted file mode 100644 index 59b11980..00000000 --- a/SAMPLE_README1.md +++ /dev/null @@ -1,177 +0,0 @@ -### 1. 프로젝트 소개 -#### 1.1. 개발배경 및 필요성 -이 프로젝트는 창의융합 해커톤에 참여하는 사람들에게 README 작성의 가이드라인을 제공하기 위해 제작되었습니다. -
- -#### 1.2. 개발목표 및 주요내용 -창의융합 해커톤을 참여하는 사람들의 README 작성방법 이해을 돕는 것입니다. -
- -#### 1.3. 세부내용 -가이드라인은 README에 들어가야 할 목차와 대략적인 내용을 설명합니다. -
- -#### 1.4. 기존 서비스(상품) 대비 차별성 -> 작성하세요. -
- -#### 1.5. 사회적가지 도입 계획 -> 작성하세요. -
- - -### 2.상세설계 -#### 2.1. 시스템 구성도 -시스템 구성도 -
- -#### 2.3. 사용기술 -| 이름 | 버전 | -|:---------------------:|:-------:| -| Python | 3.8.0 | -| Django | 3.2.9 | -| Django Rest Framework | 3.12.0 | -| Node.js | 16.16.0 | -| Vue.js | 2.5.13 | -
- - -### 3. 개발결과 -[코딩역량강화플랫폼 Online Judge](http://10.125.121.115:8080/)를 예시로 작성하였습니다. -#### 3.1. 전체시스템 흐름도 -- 유저 플로우 차트 - > 코딩 역량강화 플랫폼의 회원가입 부분만 작성했습니다.
- > 사용자의 행동 흐름을 도식화하여 보여줍니다. - 유저 플로우 차트 - -- 테스크 플로우 차트 - > 코딩 역량강화 플랫폼의 로그인 부분만 작성했습니다.
- > 주요 테스크의 프로세스를 도식화하여 보여줍니다. - 테스크 플로우 차트 - -- 시스템 플로우 차트 - > 코딩 역량강화 플랫폼의 로그인 부분만 작성했습니다.
- > 테스크의 흐름에 따른 데이터 처리를 도식화하여 보여줍니다. - 시스템 플로우 차트 - - -- IA(Information Architecture) - > 정보나 시스템의 구조를 도식화하여 보여줍니다.
- IA - -
- -#### 3.2. 기능설명 -##### ` 메인 페이지 ` -- 상단 배너 - - 3초에 마다 자동으로 내용이 넘어갑니다.
- ![상단 배너](https://github.com/pnuswedu/SW-Hackathon-2024/assets/34933690/4640389f-dcaf-4b78-916e-188c8e9c6ee7) - -- 공지사항 - - 최근 5개의 공지사항을 보여줍니다. - - 발행된지 일주일이 안 된 공지사항은 new라는 mark표시를 해줍니다. - - 공지사항 글을 클릭하면 해당 공지사항 게시글로 이동합니다. - - 상단의 더보기 버튼을 클릭하면 공지사항 페이지로 이동합니다.
- 공지사항 - -- 이번 주 보너스 문제 - - 이번 주의 보너스 점수를 주는 문제를 보여줍니다. - - 문제를 클릭하면, 해당 문제의 게시글로 이동합니다.
- 이번 주 보너스 문제 - -- 실시간 랭킹 - - 상위 랭킹 10명의 유저를 보여줍니다. - - 상단의 더보기 버튼을 클릭하면 전체 랭킹 페이지로 이동합니다.
- 실시간 랭킹 -
- -##### ` 문제 페이지 ` -- 문제 목록 - - 사용자가 설정한 한 번에 보여줄 문제 갯수 만큼 한 화면에 문제를 띄워줍니다. - - 검색창에서 문제의 제목 및 번호로 문제를 검색할 수 있습니다. - - 난이도, 영역, 카테고리 별로 문제를 볼 수 있습니다. - - 상단의 shuffle 이모지를 클릭하면 랜덤으로 선택된 문제 푸는 페이지로 이동합니다. - - 목록에서 문제를 클릭하면 해당 문제를 푸는 페이지로 이동합니다. - ![문제 목록](https://github.com/pnuswedu/SW-Hackathon-2024/assets/34933690/95afd0db-b5a7-4628-ac9c-164513a9e51b) -
- - -#### 3.3. 기능명세서 -실시간 랭킹 - -|라벨|이름|상세| -|:---:|:----------------------------:|:---| -| S1 | 부산대학교 웹메일 | - 부산대 웹메일 형식인지 검증
- 중복되는 이메일인지 검증 | -| S2 | 부산대학교 웹메일 인증 코드 전송| - 클릭 시 인증 코드 메일로 전송 | -| S3 | 메일 인증 코드 | - 인증 요청 버튼 클릭 후 활성화
- 유효시간 5분| -| S4 | 메일 인증 코드 확인 | - 인증코드 검증 | -| S5 | 닉네임 | - 4 ~ 12자 영어, 숫자, '_' 가능 | -| S6 | 단과대학 선택 | -부산대학교 단과대학 리스트 보여주기 | -| S7 | 학과 선택 | - 단과대학 안의 학과 리스트 보여주기 | -| S8 | 비밀번호 | - 입력 시 텍스트 보이지 않도록 •로 표현해주기
- 6자 이상 20자 이하, 영어와 숫자 조합 필수 | -| S9 | 비밀번호 확인 | - 입력 시 텍스트 보이지 않도록 •로 표현해주기
- 비밀번호와 동일한 지 검증 | -| S10 | 회원가입 완료 | - 비어 있는 입력 칸이 없는지 검증
-메일 인증 완료했는지 확인
-조건을 만족하면 회원가입 성공| -| S11 | 로그인 | - 클릭 시 로그인 모달로 전환 | - -
- -#### 3.4. 디렉토리 구조 -``` -├── build/ # webpack 설정 파일 -├── config/ # 프로젝트 설정 파일 -├── deplay/ # 배포 설정 파일 -├── src/ # 소스 코드 -│ ├── assets/ # 이미지, 폰트 등의 정적 파일 -│ ├── pages/ # 화면에 나타나는 페이지 -│ │ ├── page1/ # 페이지1 -│ │ ├── page2/ # 페이지2 -│ │ ├── components/ # 여러 페이지에서 공통적으로 사용되는 컴포넌트 -│ ├── router/ # 라우터 -│ ├── store/ # global state store -│ ├── styles/ # 스타일 -│ ├── utils/ # 유틸리티 -├── static/ # 정적 파일 -``` -
- - -### 4. 설치 및 사용 방법 -**필요 패키지** -- 위의 사용 기술 참고 - -```bash -$ git clone https://github.com/test/test.git -$ cd test/frontend -$ npm i -$ export NODE_ENV="development" # windows: set NODE_ENV=development -$ npm run build:dll -$ export TARGET="http://localhost:8000" # windows: set NODE_ENV=http://localhost:8000 -$ npm run dev -``` -
- - -### 5. 소개 및 시연영상 -[소개 및 시연영상](https://www.youtube.com/watch?v=EfEgTrm5_u4) - -
- -### 6. 팀 소개 -| MEMBER1 | MEMBER2 | MEMBER3 | -|:-------:|:-------:|:-------:| -|MEMBER1 | MEMBER2 | MEMBER3 | -| member1@pusan.ac.kr | member2@gmail.com | member3@naver.com | -| 프론트앤드 개발 | 인프라 구축
백앤드 개발 | DB 설계
백앤드 개발 | - - -
- - -### 7. 해커톤 참여 후기 -- MEMBER1 - > 작성하세요. -- MEMBER2 - > 작성하세요. -- MEMBER3 - > 작성하세요. -
diff --git a/backend/interviewAI/interviewAI/.gitattributes b/backend/interviewAI/interviewAI/.gitattributes new file mode 100644 index 00000000..8af972cd --- /dev/null +++ b/backend/interviewAI/interviewAI/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/backend/interviewAI/interviewAI/.gitignore b/backend/interviewAI/interviewAI/.gitignore new file mode 100644 index 00000000..c2065bc2 --- /dev/null +++ b/backend/interviewAI/interviewAI/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/backend/interviewAI/interviewAI/build.gradle b/backend/interviewAI/interviewAI/build.gradle new file mode 100644 index 00000000..29749256 --- /dev/null +++ b/backend/interviewAI/interviewAI/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java' + id 'war' + id 'org.springframework.boot' version '3.5.3' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'studyGroup' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'com.h2database:h2' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/backend/interviewAI/interviewAI/gradle/wrapper/gradle-wrapper.jar b/backend/interviewAI/interviewAI/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..1b33c55b Binary files /dev/null and b/backend/interviewAI/interviewAI/gradle/wrapper/gradle-wrapper.jar differ diff --git a/backend/interviewAI/interviewAI/gradle/wrapper/gradle-wrapper.properties b/backend/interviewAI/interviewAI/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..d4081da4 --- /dev/null +++ b/backend/interviewAI/interviewAI/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/backend/interviewAI/interviewAI/gradlew b/backend/interviewAI/interviewAI/gradlew new file mode 100644 index 00000000..23d15a93 --- /dev/null +++ b/backend/interviewAI/interviewAI/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/backend/interviewAI/interviewAI/gradlew.bat b/backend/interviewAI/interviewAI/gradlew.bat new file mode 100644 index 00000000..db3a6ac2 --- /dev/null +++ b/backend/interviewAI/interviewAI/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/interviewAI/interviewAI/settings.gradle b/backend/interviewAI/interviewAI/settings.gradle new file mode 100644 index 00000000..b106dcb1 --- /dev/null +++ b/backend/interviewAI/interviewAI/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'interviewAI' diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/InterviewAiApplication.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/InterviewAiApplication.java new file mode 100644 index 00000000..24736361 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/InterviewAiApplication.java @@ -0,0 +1,13 @@ +package studyGroup.interviewAI; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class InterviewAiApplication { + + public static void main(String[] args) { + SpringApplication.run(InterviewAiApplication.class, args); + } + +} diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/ServletInitializer.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/ServletInitializer.java new file mode 100644 index 00000000..4e17cad1 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/ServletInitializer.java @@ -0,0 +1,13 @@ +package studyGroup.interviewAI; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +public class ServletInitializer extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(InterviewAiApplication.class); + } + +} diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Applicant.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Applicant.java new file mode 100644 index 00000000..eb0f562e --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Applicant.java @@ -0,0 +1,95 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +public class Applicant { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String username; + private String email; + private String location; + private String resumeFilePath; + private String githubUrl; + private String portfolioUrl; + private String portfolioFilePath; + private LocalDateTime applyAt; + + @ManyToOne + @JoinColumn(name = "position_id", nullable = false) + private Position position; + + @ManyToOne + @JoinColumn(name = "experience_id", nullable = false) + private Experience experience; + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public String getUsername() { + return username; + } + public void setUsername(String username) { + this.username = username; + } + public String getEmail() { + return email; + } + public void setEmail(String email) { + this.email = email; + } + public String getLocation() { + return location; + } + public void setLocation(String location) { + this.location = location; + } + public String getResumeFilePath() { + return resumeFilePath; + } + public void setResumeFilePath(String resumeFilePath) { + this.resumeFilePath = resumeFilePath; + } + public String getGithubUrl() { + return githubUrl; + } + public void setGithubUrl(String githubUrl) { + this.githubUrl = githubUrl; + } + public String getPortfolioUrl() { + return portfolioUrl; + } + public void setPortfolioUrl(String portfolioUrl) { + this.portfolioUrl = portfolioUrl; + } + public String getPortfolioFilePath() { + return portfolioFilePath; + } + public void setPortfolioFilePath(String portfolioFilePath) { + this.portfolioFilePath = portfolioFilePath; + } + public LocalDateTime getApplyAt() { + return applyAt; + } + public void setApplyAt(LocalDateTime applyAt) { + this.applyAt = applyAt; + } + public Position getPosition() { + return position; + } + public void setPosition(Position position) { + this.position = position; + } + public Experience getExperience() { + return experience; + } + public void setExperience(Experience experience) { + this.experience = experience; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/ApplicantTech.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/ApplicantTech.java new file mode 100644 index 00000000..4d5bb8c3 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/ApplicantTech.java @@ -0,0 +1,37 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; + +@Entity +public class ApplicantTech { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "applicant_id") + private Applicant applicant; + + @ManyToOne + @JoinColumn(name = "tech_stack_id") + private TechStack techStack; + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public Applicant getApplicant() { + return applicant; + } + public void setApplicant(Applicant applicant) { + this.applicant = applicant; + } + public TechStack getTechStack() { + return techStack; + } + public void setTechStack(TechStack techStack) { + this.techStack = techStack; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Conversation.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Conversation.java new file mode 100644 index 00000000..456bdb1d --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Conversation.java @@ -0,0 +1,45 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; + +@Entity +public class Conversation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "interview_id", nullable = false) + private Interview interview; + + @Column(nullable = false) + private String content; + + @Column(nullable = false) + private Boolean isManager; + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public Interview getInterview() { + return interview; + } + public void setInterview(Interview interview) { + this.interview = interview; + } + public String getContent() { + return content; + } + public void setContent(String content) { + this.content = content; + } + public Boolean getIsManager() { + return isManager; + } + public void setIsManager(Boolean isManager) { + this.isManager = isManager; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Experience.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Experience.java new file mode 100644 index 00000000..e2efb595 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Experience.java @@ -0,0 +1,26 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; + +@Entity +public class Experience { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public String getTitle() { + return title; + } + public void setTitle(String title) { + this.title = title; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Interview.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Interview.java new file mode 100644 index 00000000..482dba01 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Interview.java @@ -0,0 +1,46 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +public class Interview { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "manager_id", nullable = false) + private Manager manager; + + @ManyToOne + @JoinColumn(name = "applicant_id") + private Applicant applicant; + + private LocalDateTime doneAt; + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public Manager getManager() { + return manager; + } + public void setManager(Manager manager) { + this.manager = manager; + } + public Applicant getApplicant() { + return applicant; + } + public void setApplicant(Applicant applicant) { + this.applicant = applicant; + } + public LocalDateTime getDoneAt() { + return doneAt; + } + public void setDoneAt(LocalDateTime doneAt) { + this.doneAt = doneAt; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Manager.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Manager.java new file mode 100644 index 00000000..e0795077 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Manager.java @@ -0,0 +1,17 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; + +@Entity +public class Manager { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Position.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Position.java new file mode 100644 index 00000000..d25e4e4f --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/Position.java @@ -0,0 +1,27 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; + +@Entity +public class Position { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + // getter, setter + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public String getTitle() { + return title; + } + public void setTitle(String title) { + this.title = title; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/PostReport.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/PostReport.java new file mode 100644 index 00000000..92bcb9df --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/PostReport.java @@ -0,0 +1,42 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; + +@Entity +public class PostReport { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "interview_id") + private Interview interview; + + private Integer score; + private String description; + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public Interview getInterview() { + return interview; + } + public void setInterview(Interview interview) { + this.interview = interview; + } + public Integer getScore() { + return score; + } + public void setScore(Integer score) { + this.score = score; + } + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/PreReport.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/PreReport.java new file mode 100644 index 00000000..eb6d43f7 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/PreReport.java @@ -0,0 +1,55 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; + +@Entity +public class PreReport { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "applicant_id") + private Applicant applicant; + + @ManyToOne + @JoinColumn(name = "work_history_id") + private WorkHistory workHistory; + + @ManyToOne + @JoinColumn(name = "project_history_id") + private ProjectHistory projectHistory; + + private String description; + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public Applicant getApplicant() { + return applicant; + } + public void setApplicant(Applicant applicant) { + this.applicant = applicant; + } + public WorkHistory getWorkHistory() { + return workHistory; + } + public void setWorkHistory(WorkHistory workHistory) { + this.workHistory = workHistory; + } + public ProjectHistory getProjectHistory() { + return projectHistory; + } + public void setProjectHistory(ProjectHistory projectHistory) { + this.projectHistory = projectHistory; + } + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/ProjectHistory.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/ProjectHistory.java new file mode 100644 index 00000000..53557397 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/ProjectHistory.java @@ -0,0 +1,52 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; + +@Entity +public class ProjectHistory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "applicant_id") + private Applicant applicant; + + private String title; + private String description; + + @ManyToOne + @JoinColumn(name = "tech_stack_id", nullable = false) + private TechStack techStack; + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public Applicant getApplicant() { + return applicant; + } + public void setApplicant(Applicant applicant) { + this.applicant = applicant; + } + public String getTitle() { + return title; + } + public void setTitle(String title) { + this.title = title; + } + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + public TechStack getTechStack() { + return techStack; + } + public void setTechStack(TechStack techStack) { + this.techStack = techStack; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/TechStack.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/TechStack.java new file mode 100644 index 00000000..6bdf2141 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/TechStack.java @@ -0,0 +1,25 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; + +@Entity +public class TechStack { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public String getTitle() { + return title; + } + public void setTitle(String title) { + this.title = title; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/WorkHistory.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/WorkHistory.java new file mode 100644 index 00000000..d5c7ddcd --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/entity/WorkHistory.java @@ -0,0 +1,61 @@ +package studyGroup.interviewAI.entity; + +import jakarta.persistence.*; +import java.time.LocalDate; + +@Entity +public class WorkHistory { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "applicant_id") + private Applicant applicant; + + private String companyName; + + @ManyToOne + @JoinColumn(name = "position_id", nullable = false) + private Position position; + + private LocalDate startDate; + private LocalDate endDate; + + public Long getId() { + return id; + } + public void setId(Long id) { + this.id = id; + } + public Applicant getApplicant() { + return applicant; + } + public void setApplicant(Applicant applicant) { + this.applicant = applicant; + } + public String getCompanyName() { + return companyName; + } + public void setCompanyName(String companyName) { + this.companyName = companyName; + } + public Position getPosition() { + return position; + } + public void setPosition(Position position) { + this.position = position; + } + public LocalDate getStartDate() { + return startDate; + } + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + public LocalDate getEndDate() { + return endDate; + } + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ApplicantRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ApplicantRepository.java new file mode 100644 index 00000000..3cdacc3d --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ApplicantRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.Applicant; + +public interface ApplicantRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ApplicantTechRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ApplicantTechRepository.java new file mode 100644 index 00000000..66b69c58 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ApplicantTechRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.ApplicantTech; + +public interface ApplicantTechRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ConversationRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ConversationRepository.java new file mode 100644 index 00000000..b81fa777 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ConversationRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.Conversation; + +public interface ConversationRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ExperienceRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ExperienceRepository.java new file mode 100644 index 00000000..0b09ea61 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ExperienceRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.Experience; + +public interface ExperienceRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/InterviewRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/InterviewRepository.java new file mode 100644 index 00000000..bff2aacd --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/InterviewRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.Interview; + +public interface InterviewRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ManagerRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ManagerRepository.java new file mode 100644 index 00000000..ca386ccf --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ManagerRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.Manager; + +public interface ManagerRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/PositionRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/PositionRepository.java new file mode 100644 index 00000000..f0e61fa4 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/PositionRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.Position; + +public interface PositionRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/PostReportRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/PostReportRepository.java new file mode 100644 index 00000000..aacab877 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/PostReportRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.PostReport; + +public interface PostReportRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/PreReportRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/PreReportRepository.java new file mode 100644 index 00000000..aee9bce6 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/PreReportRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.PreReport; + +public interface PreReportRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ProjectHistoryRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ProjectHistoryRepository.java new file mode 100644 index 00000000..ffb1b4be --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/ProjectHistoryRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.ProjectHistory; + +public interface ProjectHistoryRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/TechStackRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/TechStackRepository.java new file mode 100644 index 00000000..92903f3b --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/TechStackRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.TechStack; + +public interface TechStackRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/WorkHistoryRepository.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/WorkHistoryRepository.java new file mode 100644 index 00000000..ca159f3c --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/repository/WorkHistoryRepository.java @@ -0,0 +1,7 @@ +package studyGroup.interviewAI.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import studyGroup.interviewAI.entity.WorkHistory; + +public interface WorkHistoryRepository extends JpaRepository { +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ApplicantService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ApplicantService.java new file mode 100644 index 00000000..c6c96a8a --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ApplicantService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.Applicant; +import studyGroup.interviewAI.repository.ApplicantRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class ApplicantService { + private final ApplicantRepository applicantRepository; + + public ApplicantService(ApplicantRepository applicantRepository) { + this.applicantRepository = applicantRepository; + } + + public List findAll() { + return applicantRepository.findAll(); + } + + public Optional findById(Long id) { + return applicantRepository.findById(id); + } + + public Applicant save(Applicant applicant) { + return applicantRepository.save(applicant); + } + + public void deleteById(Long id) { + applicantRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ApplicantTechService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ApplicantTechService.java new file mode 100644 index 00000000..c9ca9d8b --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ApplicantTechService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.ApplicantTech; +import studyGroup.interviewAI.repository.ApplicantTechRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class ApplicantTechService { + private final ApplicantTechRepository applicantTechRepository; + + public ApplicantTechService(ApplicantTechRepository applicantTechRepository) { + this.applicantTechRepository = applicantTechRepository; + } + + public List findAll() { + return applicantTechRepository.findAll(); + } + + public Optional findById(Long id) { + return applicantTechRepository.findById(id); + } + + public ApplicantTech save(ApplicantTech applicantTech) { + return applicantTechRepository.save(applicantTech); + } + + public void deleteById(Long id) { + applicantTechRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ConversationService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ConversationService.java new file mode 100644 index 00000000..003dea5c --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ConversationService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.Conversation; +import studyGroup.interviewAI.repository.ConversationRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class ConversationService { + private final ConversationRepository conversationRepository; + + public ConversationService(ConversationRepository conversationRepository) { + this.conversationRepository = conversationRepository; + } + + public List findAll() { + return conversationRepository.findAll(); + } + + public Optional findById(Long id) { + return conversationRepository.findById(id); + } + + public Conversation save(Conversation conversation) { + return conversationRepository.save(conversation); + } + + public void deleteById(Long id) { + conversationRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ExperienceService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ExperienceService.java new file mode 100644 index 00000000..ea9f72bc --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ExperienceService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.Experience; +import studyGroup.interviewAI.repository.ExperienceRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class ExperienceService { + private final ExperienceRepository experienceRepository; + + public ExperienceService(ExperienceRepository experienceRepository) { + this.experienceRepository = experienceRepository; + } + + public List findAll() { + return experienceRepository.findAll(); + } + + public Optional findById(Long id) { + return experienceRepository.findById(id); + } + + public Experience save(Experience experience) { + return experienceRepository.save(experience); + } + + public void deleteById(Long id) { + experienceRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/InterviewService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/InterviewService.java new file mode 100644 index 00000000..b79496ca --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/InterviewService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.Interview; +import studyGroup.interviewAI.repository.InterviewRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class InterviewService { + private final InterviewRepository interviewRepository; + + public InterviewService(InterviewRepository interviewRepository) { + this.interviewRepository = interviewRepository; + } + + public List findAll() { + return interviewRepository.findAll(); + } + + public Optional findById(Long id) { + return interviewRepository.findById(id); + } + + public Interview save(Interview interview) { + return interviewRepository.save(interview); + } + + public void deleteById(Long id) { + interviewRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ManagerService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ManagerService.java new file mode 100644 index 00000000..af8702a6 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ManagerService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.Manager; +import studyGroup.interviewAI.repository.ManagerRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class ManagerService { + private final ManagerRepository managerRepository; + + public ManagerService(ManagerRepository managerRepository) { + this.managerRepository = managerRepository; + } + + public List findAll() { + return managerRepository.findAll(); + } + + public Optional findById(Long id) { + return managerRepository.findById(id); + } + + public Manager save(Manager manager) { + return managerRepository.save(manager); + } + + public void deleteById(Long id) { + managerRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/PositionService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/PositionService.java new file mode 100644 index 00000000..18ff71fd --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/PositionService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.Position; +import studyGroup.interviewAI.repository.PositionRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class PositionService { + private final PositionRepository positionRepository; + + public PositionService(PositionRepository positionRepository) { + this.positionRepository = positionRepository; + } + + public List findAll() { + return positionRepository.findAll(); + } + + public Optional findById(Long id) { + return positionRepository.findById(id); + } + + public Position save(Position position) { + return positionRepository.save(position); + } + + public void deleteById(Long id) { + positionRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/PostReportService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/PostReportService.java new file mode 100644 index 00000000..305cf0ef --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/PostReportService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.PostReport; +import studyGroup.interviewAI.repository.PostReportRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class PostReportService { + private final PostReportRepository postReportRepository; + + public PostReportService(PostReportRepository postReportRepository) { + this.postReportRepository = postReportRepository; + } + + public List findAll() { + return postReportRepository.findAll(); + } + + public Optional findById(Long id) { + return postReportRepository.findById(id); + } + + public PostReport save(PostReport postReport) { + return postReportRepository.save(postReport); + } + + public void deleteById(Long id) { + postReportRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/PreReportService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/PreReportService.java new file mode 100644 index 00000000..fb642efb --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/PreReportService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.PreReport; +import studyGroup.interviewAI.repository.PreReportRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class PreReportService { + private final PreReportRepository preReportRepository; + + public PreReportService(PreReportRepository preReportRepository) { + this.preReportRepository = preReportRepository; + } + + public List findAll() { + return preReportRepository.findAll(); + } + + public Optional findById(Long id) { + return preReportRepository.findById(id); + } + + public PreReport save(PreReport preReport) { + return preReportRepository.save(preReport); + } + + public void deleteById(Long id) { + preReportRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ProjectHistoryService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ProjectHistoryService.java new file mode 100644 index 00000000..313c1e99 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/ProjectHistoryService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.ProjectHistory; +import studyGroup.interviewAI.repository.ProjectHistoryRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class ProjectHistoryService { + private final ProjectHistoryRepository projectHistoryRepository; + + public ProjectHistoryService(ProjectHistoryRepository projectHistoryRepository) { + this.projectHistoryRepository = projectHistoryRepository; + } + + public List findAll() { + return projectHistoryRepository.findAll(); + } + + public Optional findById(Long id) { + return projectHistoryRepository.findById(id); + } + + public ProjectHistory save(ProjectHistory projectHistory) { + return projectHistoryRepository.save(projectHistory); + } + + public void deleteById(Long id) { + projectHistoryRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/TechStackService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/TechStackService.java new file mode 100644 index 00000000..3aa0f941 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/TechStackService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.TechStack; +import studyGroup.interviewAI.repository.TechStackRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class TechStackService { + private final TechStackRepository techStackRepository; + + public TechStackService(TechStackRepository techStackRepository) { + this.techStackRepository = techStackRepository; + } + + public List findAll() { + return techStackRepository.findAll(); + } + + public Optional findById(Long id) { + return techStackRepository.findById(id); + } + + public TechStack save(TechStack techStack) { + return techStackRepository.save(techStack); + } + + public void deleteById(Long id) { + techStackRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/WorkHistoryService.java b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/WorkHistoryService.java new file mode 100644 index 00000000..84b42be3 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/java/studyGroup/interviewAI/service/WorkHistoryService.java @@ -0,0 +1,33 @@ +package studyGroup.interviewAI.service; + +import org.springframework.stereotype.Service; +import studyGroup.interviewAI.entity.WorkHistory; +import studyGroup.interviewAI.repository.WorkHistoryRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class WorkHistoryService { + private final WorkHistoryRepository workHistoryRepository; + + public WorkHistoryService(WorkHistoryRepository workHistoryRepository) { + this.workHistoryRepository = workHistoryRepository; + } + + public List findAll() { + return workHistoryRepository.findAll(); + } + + public Optional findById(Long id) { + return workHistoryRepository.findById(id); + } + + public WorkHistory save(WorkHistory workHistory) { + return workHistoryRepository.save(workHistory); + } + + public void deleteById(Long id) { + workHistoryRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/main/resources/application.properties b/backend/interviewAI/interviewAI/src/main/resources/application.properties new file mode 100644 index 00000000..f347c6d8 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/main/resources/application.properties @@ -0,0 +1,17 @@ +spring.application.name=interviewAI +# H2 DB 설정 +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +# 콘솔로 DB 확인 가능 +## 실행되는 SQL 쿼리 로그로 보기 +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# JPA 자동 테이블 생성 + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +## Entity 바뀌면 테이블도 자동 반영 +spring.jpa.hibernate.ddl-auto=update \ No newline at end of file diff --git a/backend/interviewAI/interviewAI/src/test/java/studyGroup/interviewAI/InterviewAiApplicationTests.java b/backend/interviewAI/interviewAI/src/test/java/studyGroup/interviewAI/InterviewAiApplicationTests.java new file mode 100644 index 00000000..55c69d79 --- /dev/null +++ b/backend/interviewAI/interviewAI/src/test/java/studyGroup/interviewAI/InterviewAiApplicationTests.java @@ -0,0 +1,13 @@ +package studyGroup.interviewAI; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class InterviewAiApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/db_schema.dbml b/db_schema.dbml new file mode 100644 index 00000000..fab9b023 --- /dev/null +++ b/db_schema.dbml @@ -0,0 +1,83 @@ +Table manager { + id integer [primary key] +} + +Table interview { + id integer [primary key] + manager_id integer [not null, ref: - manager.id] + applicant_id integer [ref: - applicant.id] + done_at timestamp +} + +Table conversation { + id integer [primary key] + interview_id integer [not null, ref: > interview.id] + content varchar [not null] + is_manager bool [not null] +} + +Table applicant { + id integer [primary key] + username varchar + email varchar + position_id integer [not null, ref: - position.id] + experience_id integer [not null, ref: - experience.id] + location varchar + resume_file_path varchar + github_url varchar + portfolio_url varchar + portfolio_file_path varchar + apply_at timestamp +} + +Table applicant_tech { + applicant_id integer [ref: - applicant.id] + tech_stack_id integer [ref: - tech_stack.id] +} + +Table pre_report { + id integer [primary key] + applicant_id integer [ref: - applicant.id] + work_history_id integer [ref: > work_history.id] + project_history_id integer [ref: > project_history.id] + description varchar [note: '평가'] +} + +Table post_report { + id integer [primary key] + interview_id integer [ref: - interview.id] + score integer + description varchar [note: '평가'] +} + +Table work_history { + id integer [primary key] + applicant_id integer [ref: - applicant.id] + company_name varchar + position_id integer [not null, ref: - position.id] + start_date date + end_date date +} + +Table project_history { + id integer [primary key] + applicant_id integer [ref: - applicant.id] + title varchar + description varchar + tech_stack_id integer [not null, ref: > tech_stack.id] +} + +Table tech_stack { + id integer [primary key] + title varchar +} + +Table experience { + id integer [primary key, note: '이름 좀 아쉬움'] + title varchar [not null] +} + +Table position { + id integer [primary key] + title varchar [not null, note: '포지션 이름 ex) 프론트, 백, AI'] +} \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..5ef6a520 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..e215bc4c --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/app/api/speech/stream/route.ts b/frontend/app/api/speech/stream/route.ts new file mode 100644 index 00000000..d4e0e229 --- /dev/null +++ b/frontend/app/api/speech/stream/route.ts @@ -0,0 +1,133 @@ +import speech from '@google-cloud/speech' +import { NextRequest, NextResponse } from 'next/server' + +const client = new speech.SpeechClient() + +// 전역 스트림 관리 +interface StreamData { + stream: ReturnType + lastResult: { + transcript: string + isFinal: boolean + timestamp: number + } | null + audioBuffer: Buffer[] +} + +const activeStreams = new Map() + +export async function POST(request: NextRequest) { + try { + const { action, sessionId, audioData } = await request.json() + + if (action === 'start') { + return startRecognition(sessionId) + } else if (action === 'audio') { + return processAudio(sessionId, audioData) + } else if (action === 'stop') { + return stopRecognition(sessionId) + } + + return NextResponse.json({ error: 'Invalid action' }, { status: 400 }) + } catch (error) { + console.error('API Error:', error) + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }) + } +} + +function startRecognition(sessionId: string) { + const config = { + encoding: 'WEBM_OPUS' as const, + sampleRateHertz: 48000, + languageCode: 'ko-KR', + enableAutomaticPunctuation: true, + model: 'latest_long', + } + + const request = { + config, + interimResults: true, + } + + const recognizeStream = client + .streamingRecognize(request) + .on('error', (err) => { + console.error('Speech API Error:', err) + activeStreams.delete(sessionId) + }) + .on('data', (data) => { + // 스트림 데이터를 저장하여 클라이언트가 polling으로 가져갈 수 있도록 함 + const streamData = activeStreams.get(sessionId) + if (streamData) { + streamData.lastResult = { + transcript: data.results[0]?.alternatives[0]?.transcript || '', + isFinal: data.results[0]?.isFinal || false, + timestamp: Date.now() + } + } + }) + + activeStreams.set(sessionId, { + stream: recognizeStream, + lastResult: null, + audioBuffer: [] + }) + + return NextResponse.json({ status: 'started', sessionId }) +} + +function processAudio(sessionId: string, audioData: string) { + const streamData = activeStreams.get(sessionId) + if (!streamData || !streamData.stream) { + return NextResponse.json({ error: 'No active stream' }, { status: 400 }) + } + + try { + const audioBuffer = Buffer.from(audioData, 'base64') + streamData.stream.write(audioBuffer) + + // 최신 결과 반환 + const result = streamData.lastResult + streamData.lastResult = null // 읽은 후 초기화 + + return NextResponse.json({ + status: 'processing', + result: result + }) + } catch (error) { + console.error('Audio processing error:', error) + return NextResponse.json({ error: 'Audio processing failed' }, { status: 500 }) + } +} + +function stopRecognition(sessionId: string) { + const streamData = activeStreams.get(sessionId) + if (streamData && streamData.stream) { + streamData.stream.end() + activeStreams.delete(sessionId) + } + + return NextResponse.json({ status: 'stopped' }) +} + +// GET 요청으로 최신 결과 polling +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + const sessionId = searchParams.get('sessionId') + + if (!sessionId) { + return NextResponse.json({ error: 'sessionId required' }, { status: 400 }) + } + + const streamData = activeStreams.get(sessionId) + if (!streamData) { + return NextResponse.json({ error: 'No active stream' }, { status: 404 }) + } + + const result = streamData.lastResult + streamData.lastResult = null // 읽은 후 초기화 + + return NextResponse.json({ + result: result + }) +} \ No newline at end of file diff --git a/frontend/app/candidates/[id]/page.tsx b/frontend/app/candidates/[id]/page.tsx new file mode 100644 index 00000000..c02a47d0 --- /dev/null +++ b/frontend/app/candidates/[id]/page.tsx @@ -0,0 +1,462 @@ +"use client" + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Progress } from "@/components/ui/progress" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { getCandidateById } from "@/lib/mock-data" +import { + AlertTriangle, + Calendar, + CheckCircle, + Clock, + Download, + ExternalLink, + FileText, + Github, + MapPin, + Play, + Star, + TrendingUp, +} from "lucide-react" +import Link from "next/link" +import { useParams } from "next/navigation" +import { useState } from "react" +import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts" + +export default function CandidateDetailPage() { + const params = useParams() + const candidateId = params.id as string + const candidate = getCandidateById(candidateId) + + // Remove all upload-related state and functions + const [githubUrl, setGithubUrl] = useState(candidate?.githubUrl || "") + const [portfolioUrl, setPortfolioUrl] = useState(candidate?.portfolioUrl || "") + const [resumeUploaded, setResumeUploaded] = useState(candidate?.resumeUploaded || false) + + if (!candidate) { + return ( +
+
+

지원자를 찾을 수 없습니다

+

찾으시는 지원자가 존재하지 않습니다.

+ + + +
+
+ ) + } + + // Mock performance data for completed interviews + const performanceData = + candidate.status === "completed" + ? [ + { name: "기술 지식", value: 85, color: "#3b82f6" }, + { name: "문제 해결", value: 78, color: "#10b981" }, + { name: "커뮤니케이션", value: 92, color: "#f59e0b" }, + { name: "코드 품질", value: 88, color: "#8b5cf6" }, + ] + : [] + + const topicData = + candidate.status === "completed" + ? [ + { topic: "React", timeSpent: 12, questions: 4, success: 85 }, + { topic: "TypeScript", timeSpent: 8, questions: 3, success: 90 }, + { topic: "알고리즘", timeSpent: 15, questions: 5, success: 70 }, + { topic: "시스템 설계", timeSpent: 10, questions: 2, success: 75 }, + ] + : [] + + const downloadReport = () => { + // Simulate PDF download + const link = document.createElement("a") + link.href = "#" + link.download = `${candidate.name.replace(" ", "_")}_interview_report.pdf` + link.click() + } + + const getStatusText = (status: string) => { + switch (status) { + case "completed": + return "완료" + case "pending": + return "대기중" + default: + return status + } + } + + return ( +
+
+ {/* Header */} +
+
+
+ + + + {candidate.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

{candidate.name}

+

{candidate.position}

+
+ + + {candidate.experience} + + + + {candidate.location} + + + + {new Date(candidate.appliedDate).toLocaleDateString()} 지원 + +
+
+
+
+ + {getStatusText(candidate.status)} + + + + +
+
+ + {/* Skills */} +
+

기술 스택:

+
+ {candidate.skills.map((skill) => ( + + {skill} + + ))} +
+
+
+ + {/* Tabs */} + + + 기술 요약 + + 면접 리포트 + + + + {/* Skills Summary Tab */} + +
+ {/* Pre-Interview Analysis */} + + + + + 이력서 분석 + + 업로드된 이력서 기반 AI 분석 + + +
+
+
+
{candidate.experience}
+
경력
+
+
+
{candidate.skills.length}
+
주요 기술
+
+
+ +
+

기술 스택:

+
+ {candidate.skills.map((skill) => ( + + {skill} + + ))} +
+
+
+
+
+ + {/* GitHub Analysis */} + + + + + GitHub 분석 + + 코드 저장소 및 기여도 분석 + + + {candidate.githubUrl ? ( +
+
+
+
15
+
저장소
+
+
+
4.2k
+
기여도
+
+
+ +
+

주요 언어:

+
+ {["JavaScript", "TypeScript", "Python", "React"].map((lang) => ( + + {lang} + + ))} +
+
+ + + GitHub 프로필 보기 + +
+ ) : ( +

GitHub 프로필이 제공되지 않았습니다

+ )} +
+
+ + {/* Portfolio Analysis */} + + + + + 포트폴리오 & 프로젝트 + + 포트폴리오 및 프로젝트 작업 분석 + + + {candidate.portfolioUrl ? ( +
+
+

확인된 주요 강점:

+
    +
  • • 모던 프레임워크를 활용한 강력한 프론트엔드 개발 경험
  • +
  • • 뛰어난 UI/UX 디자인 감각
  • +
  • • 프로젝트 전반에 걸쳐 입증된 풀스택 역량
  • +
  • • 활발한 오픈소스 기여 활동
  • +
+
+ + + 포트폴리오 보기 + +
+ ) : ( +

포트폴리오가 제공되지 않았습니다

+ )} +
+
+
+
+ + {/* Interview Report Tab - keep existing content */} + + {candidate.status === "completed" && ( + <> + {/* Overall Score */} + + + 면접 성과 + + {candidate.interviewDate && new Date(candidate.interviewDate).toLocaleDateString()} 완료 + + + +
+ + + + +
+ {candidate.score}% +
+
+
+ + + {candidate.score && candidate.score >= 85 + ? "우수" + : candidate.score && candidate.score >= 70 + ? "양호" + : "개선 필요"} + +
+ +
+
+ +
+ {/* Performance Breakdown */} + + + + + 성과 분석 + + + +
+ {performanceData.map((item, index) => ( +
+
+ {item.name} + {item.value}% +
+ +
+ ))} +
+
+
+ + {/* Topic Performance */} + + + 주제별 성과 + + + + + + + + + + + + + +
+ +
+ {/* Strengths */} + + + + + 주요 강점 + + + +
    + {candidate.strengths?.map((strength, index) => ( +
  • + + {strength} +
  • + )) || [ +
  • + + 문제 해결 능력이 뛰어남 +
  • , +
  • + + 기술적 지식이 풍부함 +
  • , +
  • + + 의사소통 능력이 우수함 +
  • + ]} +
+
+
+ + {/* Areas for Improvement */} + + + + + 개선 영역 + + + +
    + {candidate.improvements?.map((improvement, index) => ( +
  • + + {improvement} +
  • + )) || [ +
  • + + 알고리즘 최적화 기법 보완 필요 +
  • , +
  • + + 시스템 설계 경험 확장 필요 +
  • + ]} +
+
+
+
+ + )} +
+
+
+
+ ) +} diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/frontend/app/favicon.ico differ diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 00000000..dc98be74 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,122 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/app/interview/[id]/page.tsx b/frontend/app/interview/[id]/page.tsx new file mode 100644 index 00000000..d55a0331 --- /dev/null +++ b/frontend/app/interview/[id]/page.tsx @@ -0,0 +1,643 @@ +"use client" + +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { getCandidateById } from "@/lib/mock-data" +import { ArrowRight, Bot, Clock, Mic, MicOff, Play, Plus, Square, User, Volume2 } from "lucide-react" +import Link from "next/link" +import { useParams } from "next/navigation" +import { useEffect, useRef, useState } from "react" + +interface Message { + id: string + sender: "candidate" | "interviewer" + content: string + timestamp: string + isTranscribing?: boolean +} + +interface Question { + id: string + category: string + difficulty: "쉬움" | "보통" | "어려움" + question: string + followUps: string[] +} + +export default function LiveInterviewPage() { + const params = useParams() + const candidateId = params.id as string + const candidate = getCandidateById(candidateId) + + const [isRecording, setIsRecording] = useState(false) + const [isInterviewStarted, setIsInterviewStarted] = useState(false) + const [messages, setMessages] = useState([]) + const [selectedDifficulty, setSelectedDifficulty] = useState("all") + const [selectedTopic, setSelectedTopic] = useState("all") + const [showQuestions, setShowQuestions] = useState(true) + const [interviewDuration, setInterviewDuration] = useState(0) + const [currentSpeaker, setCurrentSpeaker] = useState<"candidate" | "interviewer" | null>(null) + const [audioLevel, setAudioLevel] = useState(0) + const [currentTranscript, setCurrentTranscript] = useState("") + const [sessionId, setSessionId] = useState("") + + const messagesEndRef = useRef(null) + const intervalRef = useRef(null) + const mediaRecorderRef = useRef(null) + const pollingIntervalRef = useRef(null) + + const questions: Question[] = [ + { + id: "1", + category: "React", + difficulty: "보통", + question: "useEffect와 useLayoutEffect 훅의 차이점을 설명해주세요.", + followUps: [ + "언제 어떤 것을 선택해야 할까요?", + "실용적인 예시를 제공해 주실 수 있나요?", + "렌더링 사이클에 어떤 영향을 미치나요?", + ], + }, + { + id: "2", + category: "시스템 설계", + difficulty: "어려움", + question: "수백만 명의 사용자를 처리할 수 있는 실시간 채팅 애플리케이션을 설계해보세요.", + followUps: [ + "메시지 저장을 어떻게 처리하시겠습니까?", + "WebSocket 연결의 확장성은 어떻게 처리하시겠습니까?", + "메시지 전송 보장을 어떻게 구현하시겠습니까?", + ], + }, + { + id: "3", + category: "알고리즘", + difficulty: "쉬움", + question: "내장 메서드를 사용하지 않고 문자열을 뒤집는 함수를 구현해보세요.", + followUps: [ + "시간 복잡도는 어떻게 되나요?", + "공간 효율성을 위해 최적화할 수 있나요?", + "유니코드 문자는 어떻게 처리하시겠습니까?", + ], + }, + { + id: "4", + category: "TypeScript", + difficulty: "보통", + question: "제네릭 제약조건(generic constraints)을 설명하고 언제 사용하는지 예시를 들어주세요.", + followUps: [ + "타입 안전성을 어떻게 향상시키나요?", + "실제 사용 사례를 보여주실 수 있나요?", + "조건부 타입은 어떤가요?", + ], + }, + ] + + const filteredQuestions = questions.filter((q) => { + const difficultyMatch = selectedDifficulty === "all" || q.difficulty === selectedDifficulty + const topicMatch = selectedTopic === "all" || q.category.toLowerCase().includes(selectedTopic.toLowerCase()) + return difficultyMatch && topicMatch + }) + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) + }, [messages]) + + useEffect(() => { + // Generate session ID on component mount + setSessionId(Date.now().toString()) + }, []) + + useEffect(() => { + if (isInterviewStarted) { + intervalRef.current = setInterval(() => { + setInterviewDuration((prev) => prev + 1) + }, 1000) + } else { + if (intervalRef.current) { + clearInterval(intervalRef.current) + } + } + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + } + } + }, [isInterviewStarted]) + + // Simulate audio level when recording + useEffect(() => { + if (isRecording && isInterviewStarted) { + const audioInterval = setInterval(() => { + setAudioLevel(Math.random() * 100) + }, 100) + + return () => { + clearInterval(audioInterval) + } + } else { + setAudioLevel(0) + } + }, [isRecording, isInterviewStarted]) + + const startRecording = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) + const mediaRecorder = new MediaRecorder(stream, { + mimeType: 'audio/webm;codecs=opus' + }) + mediaRecorderRef.current = mediaRecorder + + // Send recording start signal to server + await fetch('/api/speech/stream', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'start', sessionId }) + }) + + mediaRecorder.ondataavailable = async (event) => { + if (event.data.size > 0) { + const reader = new FileReader() + reader.onload = async () => { + const base64Audio = (reader.result as string).split(',')[1] + + try { + const response = await fetch('/api/speech/stream', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'audio', + sessionId, + audioData: base64Audio + }) + }) + + const data = await response.json() + if (data.result && data.result.transcript) { + setCurrentTranscript(data.result.transcript) + + // Add as message if final result + if (data.result.isFinal) { + addTranscriptAsMessage(data.result.transcript) + setCurrentTranscript("") + } + } + } catch (error) { + console.error('Audio processing error:', error) + } + } + reader.readAsDataURL(event.data) + } + } + + mediaRecorder.start(1000) // Generate data chunks every 1 second + setIsRecording(true) + setCurrentSpeaker("candidate") + + // Start result polling + startPolling() + + } catch (error) { + console.error('Error starting recording:', error) + alert('마이크 접근 권한이 필요합니다.') + } + } + + const stopRecording = async () => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop() + mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop()) + mediaRecorderRef.current = null + setIsRecording(false) + setCurrentSpeaker(null) + + // Send recording stop signal to server + await fetch('/api/speech/stream', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'stop', sessionId }) + }) + + // Stop polling + if (pollingIntervalRef.current) { + clearInterval(pollingIntervalRef.current) + pollingIntervalRef.current = null + } + + // Add current transcript as message if exists + if (currentTranscript.trim()) { + addTranscriptAsMessage(currentTranscript) + setCurrentTranscript("") + } + } + } + + const startPolling = () => { + pollingIntervalRef.current = setInterval(async () => { + try { + const response = await fetch(`/api/speech/stream?sessionId=${sessionId}`) + const data = await response.json() + + if (data.result && data.result.transcript) { + setCurrentTranscript(data.result.transcript) + + if (data.result.isFinal) { + addTranscriptAsMessage(data.result.transcript) + setCurrentTranscript("") + } + } + } catch (error) { + console.error('Polling error:', error) + } + }, 500) // Poll every 0.5 seconds + } + + const addTranscriptAsMessage = (transcript: string) => { + if (transcript.trim()) { + const message: Message = { + id: Date.now().toString(), + sender: "candidate", + content: transcript, + timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), + } + setMessages((prev) => [...prev, message]) + + // Simulate interviewer response + setTimeout(() => { + const responses = [ + "좋은 답변이네요. 더 자세히 설명해 주실 수 있나요?", + "흥미로운 접근법이네요. 예외 상황은 어떻게 처리하시겠습니까?", + "잘 설명해주셨습니다. 구현에 대해 더 자세히 알아볼까요?", + "이해했습니다. 성능 고려사항은 어떤가요?", + ] + + const response: Message = { + id: (Date.now() + 1).toString(), + sender: "interviewer", + content: responses[Math.floor(Math.random() * responses.length)], + timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), + } + + setMessages((prev) => [...prev, response]) + }, 1500) + } + } + + const startInterview = () => { + setIsInterviewStarted(true) + + // Add welcome message + const welcomeMessage: Message = { + id: "welcome", + sender: "interviewer", + content: `안녕하세요 ${candidate?.name}님! 기술 면접에 오신 것을 환영합니다. ${candidate?.position} 분야의 경력을 검토해보았습니다. 먼저 본인의 경험에 대해 간단히 소개해 주세요.`, + timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), + } + + setMessages([welcomeMessage]) + } + + const stopInterview = () => { + setIsInterviewStarted(false) + stopRecording() + } + + const toggleRecording = () => { + if (isRecording) { + stopRecording() + } else { + startRecording() + } + } + + const addQuestionToChat = (question: string) => { + const message: Message = { + id: Date.now().toString(), + sender: "interviewer", + content: question, + timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), + } + setMessages((prev) => [...prev, message]) + } + + const formatDuration = (seconds: number) => { + const mins = Math.floor(seconds / 60) + const secs = seconds % 60 + return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}` + } + + const getDifficultyColor = (difficulty: string) => { + switch (difficulty) { + case "쉬움": + return "bg-green-100 text-green-800" + case "보통": + return "bg-yellow-100 text-yellow-800" + case "어려움": + return "bg-red-100 text-red-800" + default: + return "bg-gray-100 text-gray-800" + } + } + + if (!candidate) { + return ( +
+
+

지원자를 찾을 수 없습니다

+ + + +
+
+ ) + } + + return ( +
+
+ {/* Main Interview Area */} +
+ {/* Header */} +
+
+
+
+ + + {candidate.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

{candidate.name}

+

{candidate.position}

+
+
+ +
+
+
+ {isInterviewStarted ? "실시간 면접" : "시작 전"} +
+ + {isInterviewStarted && ( +
+ + {formatDuration(interviewDuration)} +
+ )} +
+
+ +
+ + + {!isInterviewStarted ? ( + + ) : ( + + + + )} +
+
+
+ + {/* Recording Status */} + {isInterviewStarted && ( +
+
+
+
+ + + {isRecording && ( +
+
+ +
+
+
+
+ + {currentSpeaker && ( + + {currentSpeaker === "candidate" ? "지원자 발화 중" : "면접관 발화 중"} + + )} +
+ )} +
+
+ + + + 🎤 실시간 음성 인식이 활성화되었습니다. 모든 대화가 자동으로 텍스트로 변환됩니다. + + +
+
+ )} + + {/* Messages */} +
+ {!isInterviewStarted ? ( +
+
+ +
+

면접 시작 준비 완료

+

+ “면접 시작”을 클릭하여 실시간 음성 인식을 시작하세요 +

+

+ 시스템이 자동으로 발화자를 구분하고 대화를 텍스트로 변환합니다 +

+
+ ) : ( + <> + {messages.map((message) => ( +
+ {message.sender === "candidate" && ( + + + + + + )} + +
+

{message.content}

+

+ {message.timestamp} +

+
+ + {message.sender === "interviewer" && ( + + + + + + )} +
+ ))} + + {/* Display current transcript */} + {currentTranscript && ( +
+ + + + + +
+

{currentTranscript}

+

변환 중...

+
+
+ )} + +
+ + )} +
+
+ + {/* Question Recommendation Sidebar */} + {showQuestions && ( +
+
+

추천 질문

+ +
+
+ + +
+ +
+ + +
+
+
+ +
+ {filteredQuestions.map((question) => ( + + +
+ + {question.category} + + + {question.difficulty} + +
+
+ +

{question.question}

+ + + + {question.followUps.length > 0 && ( +
+

후속 질문 제안:

+ {question.followUps.map((followUp, index) => ( +
+ +
+ ))} +
+ )} +
+
+ ))} +
+
+ )} +
+
+ ) +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 00000000..f7fa87eb --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 00000000..92b7a2b8 --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,330 @@ +"use client" + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { mockCandidates } from "@/lib/mock-data" +import { Calendar, Clock, Eye, Filter, MapPin, Play, Plus, RotateCcw, Search } from "lucide-react" +import Link from "next/link" +import { useMemo, useState } from "react" + +export default function OverviewPage() { + const [searchTerm, setSearchTerm] = useState("") + const [statusFilter, setStatusFilter] = useState("all") + const [positionFilter, setPositionFilter] = useState("all") + const [sortBy, setSortBy] = useState("appliedDate") + + const filteredAndSortedCandidates = useMemo(() => { + const filtered = mockCandidates.filter((candidate) => { + const matchesSearch = + candidate.name.toLowerCase().includes(searchTerm.toLowerCase()) || + candidate.email.toLowerCase().includes(searchTerm.toLowerCase()) || + candidate.position.toLowerCase().includes(searchTerm.toLowerCase()) + + const matchesStatus = statusFilter === "all" || candidate.status === statusFilter + const matchesPosition = positionFilter === "all" || candidate.position.includes(positionFilter) + + return matchesSearch && matchesStatus && matchesPosition + }) + + // Sort candidates + filtered.sort((a, b) => { + switch (sortBy) { + case "name": + return a.name.localeCompare(b.name) + case "appliedDate": + return new Date(b.appliedDate).getTime() - new Date(a.appliedDate).getTime() + case "status": + return a.status.localeCompare(b.status) + case "score": + return (b.score || 0) - (a.score || 0) + default: + return 0 + } + }) + + return filtered + }, [searchTerm, statusFilter, positionFilter, sortBy]) + + const getStatusColor = (status: string) => { + switch (status) { + case "completed": + return "bg-green-100 text-green-800 border-green-200" + case "pending": + return "bg-yellow-100 text-yellow-800 border-yellow-200" + default: + return "bg-gray-100 text-gray-800 border-gray-200" + } + } + + const getStatusText = (status: string) => { + switch (status) { + case "completed": + return "완료" + case "pending": + return "대기중" + default: + return status + } + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("ko-KR", { + month: "long", + day: "numeric", + year: "numeric", + }) + } + + const positions = Array.from(new Set(mockCandidates.map((c) => c.position.split(" ").pop() || ""))) + + return ( +
+
+ {/* Header */} +
+
+
+

지원자 현황

+

인터뷰 진행 상황을 한눈에 확인하고 관리하세요

+
+ + + +
+
+ + {/* Stats Cards */} +
+ + +
+
+

전체 지원자 수

+

{mockCandidates.length}명

+
+
+ +
+
+
+
+ + + +
+
+

인터뷰 대기중

+

+ {mockCandidates.filter((c) => c.status === "pending").length}명 +

+
+
+ +
+
+
+
+ + + +
+
+

인터뷰 완료

+

+ {mockCandidates.filter((c) => c.status === "completed").length}명 +

+
+
+ +
+
+
+
+ + + +
+
+

평균 점수

+

+ {Math.round( + mockCandidates.filter((c) => c.score).reduce((acc, c) => acc + (c.score || 0), 0) / + mockCandidates.filter((c) => c.score).length, + )}점 +

+
+
+ +
+
+
+
+
+ + {/* Filters and Search */} + + +
+
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+
+ +
+ + + + + +
+
+
+
+ + {/* Candidates Grid */} +
+ {filteredAndSortedCandidates.map((candidate) => ( + + +
+
+ + + + {candidate.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

{candidate.name}

+

{candidate.email}

+
+
+ + {getStatusText(candidate.status)} + +
+
+ + +
+

{candidate.position}

+
+ + + {candidate.experience} + + + + {candidate.location} + +
+
+ +
+

보유 스킬

+
+ {candidate.skills.slice(0, 3).map((skill) => ( + + {skill} + + ))} + {candidate.skills.length > 3 && ( + + 외 {candidate.skills.length - 3}개 + + )} +
+
+ +
+ {formatDate(candidate.appliedDate)} 지원 + {candidate.score && {candidate.score}점} +
+ +
+ + + + + + +
+
+
+ ))} +
+ + {filteredAndSortedCandidates.length === 0 && ( +
+
+ +
+

검색 결과가 없습니다

+

다른 검색어나 필터를 사용해보세요

+
+ )} +
+
+ ) +} diff --git a/frontend/app/register/page.tsx b/frontend/app/register/page.tsx new file mode 100644 index 00000000..16cc3a79 --- /dev/null +++ b/frontend/app/register/page.tsx @@ -0,0 +1,503 @@ +"use client" + +import type React from "react" + +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Progress } from "@/components/ui/progress" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" +import { CheckCircle, ExternalLink, FileText, Github, LinkIcon, Upload, User } from "lucide-react" +import { useRouter } from "next/navigation" +import { useState } from "react" + +interface FormData { + name: string + email: string + position: string + experience: string + location: string + githubUrl: string + portfolioUrl: string + skills: string +} + +interface FileUpload { + resume: File | null + portfolioFiles: File[] +} + +export default function RegisterPage() { + const router = useRouter() + const [formData, setFormData] = useState({ + name: "", + email: "", + position: "", + experience: "", + location: "", + githubUrl: "", + portfolioUrl: "", + skills: "", + }) + + const [files, setFiles] = useState({ + resume: null, + portfolioFiles: [], + }) + + const [dragActive, setDragActive] = useState({ + resume: false, + portfolio: false, + }) + + const [uploadProgress, setUploadProgress] = useState(0) + const [isSubmitting, setIsSubmitting] = useState(false) + const [errors, setErrors] = useState>({}) + const [githubValidated, setGithubValidated] = useState(false) + + const positions = [ + "프론트엔드 개발자", + "백엔드 개발자", + "풀스택 엔지니어", + "모바일 개발자", + "DevOps 엔지니어", + "데이터 엔지니어", + "UI/UX 디자이너", + "제품 매니저", + ] + + const experienceLevels = ["1-2년", "3-4년", "5-7년", "8년 이상"] + + const handleInputChange = (field: keyof FormData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })) + // Clear error when user starts typing + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: undefined })) + } + } + + const handleDrag = (e: React.DragEvent, type: "resume" | "portfolio") => { + e.preventDefault() + e.stopPropagation() + if (e.type === "dragenter" || e.type === "dragover") { + setDragActive((prev) => ({ ...prev, [type]: true })) + } else if (e.type === "dragleave") { + setDragActive((prev) => ({ ...prev, [type]: false })) + } + } + + const handleDrop = (e: React.DragEvent, type: "resume" | "portfolio") => { + e.preventDefault() + e.stopPropagation() + setDragActive((prev) => ({ ...prev, [type]: false })) + + const droppedFiles = Array.from(e.dataTransfer.files) + + if (type === "resume") { + const file = droppedFiles[0] + if (file && (file.type === "application/pdf" || file.type.includes("word"))) { + setFiles((prev) => ({ ...prev, resume: file })) + } + } else { + setFiles((prev) => ({ ...prev, portfolioFiles: [...prev.portfolioFiles, ...droppedFiles] })) + } + } + + const handleFileInput = (e: React.ChangeEvent, type: "resume" | "portfolio") => { + const selectedFiles = Array.from(e.target.files || []) + + if (type === "resume") { + const file = selectedFiles[0] + if (file) { + setFiles((prev) => ({ ...prev, resume: file })) + } + } else { + setFiles((prev) => ({ ...prev, portfolioFiles: [...prev.portfolioFiles, ...selectedFiles] })) + } + } + + const removePortfolioFile = (index: number) => { + setFiles((prev) => ({ + ...prev, + portfolioFiles: prev.portfolioFiles.filter((_, i) => i !== index), + })) + } + + const validateGithub = () => { + if (formData.githubUrl && formData.githubUrl.includes("github.com")) { + setGithubValidated(true) + } + } + + const validateForm = (): boolean => { + const newErrors: Partial = {} + + if (!formData.name.trim()) newErrors.name = "이름을 입력해주세요" + if (!formData.email.trim()) newErrors.email = "이메일을 입력해주세요" + else if (!/\S+@\S+\.\S+/.test(formData.email)) newErrors.email = "유효하지 않은 이메일 형식입니다" + if (!formData.position) newErrors.position = "포지션을 선택해주세요" + if (!formData.experience) newErrors.experience = "경력을 선택해주세요" + if (!formData.location.trim()) newErrors.location = "지역을 입력해주세요" + if (!formData.skills.trim()) newErrors.skills = "기술 스택을 입력해주세요" + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const simulateUpload = () => { + return new Promise((resolve) => { + let progress = 0 + const interval = setInterval(() => { + progress += 10 + setUploadProgress(progress) + if (progress >= 100) { + clearInterval(interval) + resolve() + } + }, 150) + }) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!validateForm()) { + return + } + + if (!files.resume) { + setErrors({ name: "이력서를 업로드해주세요" }) + return + } + + setIsSubmitting(true) + + try { + // Simulate file upload + await simulateUpload() + + // Simulate API call delay + await new Promise((resolve) => setTimeout(resolve, 1000)) + + // In a real app, you would send the data to your API here + console.log("Submitting candidate:", { formData, files }) + + // Redirect to overview page + router.push("/") + } catch (error) { + console.error("Error submitting candidate:", error) + } finally { + setIsSubmitting(false) + setUploadProgress(0) + } + } + + return ( +
+
+ {/* Header */} +
+

새 지원자 등록

+

면접 파이프라인에 새로운 지원자를 추가하세요

+
+ +
+ {/* Basic Information */} + + + + + 기본 정보 + + 지원자의 개인 정보 및 경력 사항을 입력해주세요 + + +
+
+ + handleInputChange("name", e.target.value)} + className={errors.name ? "border-red-500" : ""} + /> + {errors.name &&

{errors.name}

} +
+ +
+ + handleInputChange("email", e.target.value)} + className={errors.email ? "border-red-500" : ""} + /> + {errors.email &&

{errors.email}

} +
+
+ +
+
+ + + {errors.position &&

{errors.position}

} +
+ +
+ + + {errors.experience &&

{errors.experience}

} +
+
+ +
+ + handleInputChange("location", e.target.value)} + className={errors.location ? "border-red-500" : ""} + /> + {errors.location &&

{errors.location}

} +
+ +
+ +