diff --git a/README.md b/README.md index f29384e..3df342d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,31 @@ -# AI 검사 시스템 API +# 🧠 AI 검사 시스템 API +> **FastAPI 기반의 지능형 진로 적성 검사 및 입학 성적 분석 시스템** +> +> 흥미검사(HMT)와 직업적성검사(CST)를 AI로 분석하고, 입학 성적 데이터를 자동으로 매핑하여 맞춤형 리포트를 제공하는 종합 REST API 서비스 -### 🔍 주요 검사 유형 +## 📋 목차 + +- [프로젝트 개요](#-프로젝트-개요) +- [주요 기능](#-주요-기능) +- [기술 스택](#-기술-스택) +- [프로젝트 구조](#-프로젝트-구조) +- [시작하기](#-시작하기) +- [API 문서](#-api-문서) +- [핵심 아키텍처](#-핵심-아키텍처) +- [성능 최적화](#-성능-최적화) +- [에러 처리](#-에러-처리) +- [테스트](#-테스트) +- [배포](#-배포) + +## 🎯 프로젝트 개요 + +### 🔍 검사 유형 #### 📊 흥미검사 (HMT - Holland's Theory) - **RIASEC 모델** 기반 6개 흥미 영역 분석 - **R**: 현실형 (Realistic) - 실용적, 기계적 -- **I**: 탐구형 (Investigative) - 분석적, 지적 +- **I**: 탐구형 (Investigative) - 분석적, 지적 - **A**: 예술형 (Artistic) - 창의적, 표현적 - **S**: 사회형 (Social) - 협력적, 교육적 - **E**: 진취형 (Enterprising) - 설득적, 관리적 @@ -17,118 +36,179 @@ - 수리·논리력, 예술시각능력, 손재능, 공간지각력, 음악능력 - 대인관계능력, 창의력, 언어능력, 신체·운동능력, 자연친화력, 자기성찰능력 -## 🏗️ 아키텍처 +### 🚀 핵심 특징 +- **AI 기반 분석**: OpenAI GPT-4o를 활용한 맞춤형 리포트 생성 +- **PDF 자동 처리**: 검사 결과 PDF에서 점수 자동 추출 +- **실시간 분석**: 업로드 즉시 검사 결과 분석 및 저장 +- **보안 인증**: JWT + OAuth 통합 인증 시스템 +- **클라우드 연동**: AWS S3를 통한 파일 저장 및 관리 +- **입학 성적 매핑**: CSV 데이터를 DB와 자동 매핑하여 입학 가능성 분석 +- **유사도 기반 검색**: 대학교명과 학과명의 유사도를 통한 정확한 매칭 -### 📁 프로젝트 구조 -``` -AI/ -├── app/ # 메인 애플리케이션 -│ ├── run.py # FastAPI 서버 실행 파일 -│ ├── db.py # 데이터베이스 설정 -│ ├── domain/ # 도메인 모델 -│ │ ├── entity/ -│ │ │ └── BaseEntity.py # 기본 엔티티 (생성/수정 시간 자동 관리) -│ │ ├── User.py # 사용자 엔티티 -│ │ ├── Hmt.py # 흥미검사 엔티티 -│ │ ├── Cst.py # 직업적성검사 엔티티 -│ │ ├── AiReport.py # AI 분석 리포트 엔티티 -│ │ ├── Report.py # 리포트 엔티티 -│ │ ├── ReportScore.py # 리포트 점수 엔티티 -│ │ ├── Mock.py # 모의고사 엔티티 -│ │ ├── MockScore.py # 모의고사 점수 엔티티 -│ │ ├── Major.py # 전공 엔티티 -│ │ ├── Field.py # 분야 엔티티 -│ │ ├── University.py # 대학교 엔티티 -│ │ ├── UniversityMajor.py # 대학-전공 연관 엔티티 -│ │ └── OAuth.py # OAuth 인증 엔티티 -│ ├── DTO/ # 데이터 전송 객체 -│ │ ├── HmtDTO.py # 흥미검사 DTO -│ │ ├── CstDTO.py # 직업적성검사 DTO -│ │ ├── UserDTO.py # 사용자 DTO -│ │ └── AiRepotDto.py # AI 리포트 DTO -│ ├── repository/ # 데이터 접근 계층 -│ │ ├── Repository.py # 기본 레포지토리 (CRUD 공통 로직) -│ │ ├── userRepository.py # 사용자 레포지토리 -│ │ ├── hmtRepository.py # 흥미검사 레포지토리 -│ │ ├── cstRepository.py # 직업적성검사 레포지토리 -│ │ ├── aiReportRepository.py # AI 리포트 레포지토리 -│ │ ├── reportRepository.py # 리포트 레포지토리 -│ │ ├── mockRepository.py # 모의고사 레포지토리 -│ │ ├── majorRepository.py # 전공 레포지토리 -│ │ ├── fieldRepository.py # 분야 레포지토리 -│ │ ├── universityRepository.py # 대학교 레포지토리 -│ │ └── oauthRepository.py # OAuth 레포지토리 -│ ├── services/ # 비즈니스 로직 -│ │ ├── HmtService.py # 흥미검사 서비스 -│ │ ├── CstService.py # 직업적성검사 서비스 -│ │ ├── UserService.py # 사용자 서비스 -│ │ ├── AiReportService.py # AI 리포트 서비스 -│ │ └── ReportService.py # 리포트 서비스 -│ ├── routes/ # API 라우터 -│ │ ├── HmtController.py # 흥미검사 컨트롤러 -│ │ ├── CstController.py # 직업적성검사 컨트롤러 -│ │ └── AuthController.py # 인증 컨트롤러 -│ ├── login/ # 인증 시스템 -│ │ ├── jwt_auth.py # JWT 인증 서비스 -│ │ ├── jwt_util.py # JWT 유틸리티 -│ │ ├── oauth_jwt_auth.py # OAuth + JWT 통합 인증 -│ │ ├── redis_auth.py # Redis 인증 -│ │ └── jwtUser.py # JWT 사용자 모델 -│ ├── gptApi/ # GPT API 연동 -│ │ ├── gptEngine.py # GPT 엔진 (추상 클래스) -│ │ ├── testReportEng/ # 테스트 리포트 엔진 -│ │ │ └── testReport.py # 테스트 리포트 생성 -│ │ └── test.py # GPT API 테스트 -│ ├── util/ # 유틸리티 -│ │ ├── Transactional.py # 트랜잭션 데코레이터 (읽기/쓰기 분리) -│ │ ├── globalDB/ # 글로벌 DB 컨텍스트 -│ │ │ ├── db_context.py # DB 컨텍스트 관리 -│ │ │ └── global_db.py # 글로벌 DB 접근 -│ │ ├── PdfExtracter/ # PDF 처리 엔진 -│ │ │ ├── HmtExtracter.py # 흥미검사 PDF 분석 -│ │ │ └── CstExtracter.py # 직업적성검사 PDF 분석 -│ │ ├── S3/ # AWS S3 연동 -│ │ │ └── S3_Util.py # S3 파일 업로드/다운로드 -│ │ ├── auth_dependency.py # 인증 의존성 -│ │ └── termGenerator.py # 학기 생성기 -│ ├── globals/ # 전역 설정 -│ │ ├── exceptions.py # 커스텀 예외 및 에러코드 -│ │ ├── exception_handler.py # 전역 예외 처리기 -│ │ └── error_codes.md # 에러코드 문서 -│ ├── redisClient.py # Redis 클라이언트 -│ └── __init__.py -├── PdfExtractor/ # PDF 추출 테스트 -├── Test/ # 테스트 코드 -├── pytest.ini # pytest 설정 -├── requirements.txt # Python 의존성 -└── README.md # 프로젝트 문서 -``` +## 🛠️ 주요 기능 -## 🛠️ 기술 스택 +### 📄 PDF 처리 엔진 +- **자동 점수 추출**: 검사 결과 PDF에서 정확한 점수 자동 인식 +- **파일 검증**: PDF 형식, 페이지 수, 내용 키워드 자동 확인 +- **에러 방지**: 잘못된 PDF 업로드 시 적절한 안내 메시지 + +### 🤖 AI 분석 엔진 +- **맞춤형 리포트**: 개인별 검사 결과에 따른 개인화된 분석 +- **JSON 응답**: 구조화된 데이터로 일관된 응답 형식 +- **토큰 최적화**: 효율적인 GPT API 사용으로 비용 절약 + +### 🎓 입학 성적 분석 시스템 +- **CSV 자동 매핑**: 입학 성적 데이터를 대학교/학과와 자동 연결 +- **유사도 검사**: 대학교명과 학과명의 유사도를 통한 정확한 매칭 +- **성적 비교**: 사용자 교과 성적과 입학 컷 비교 분석 +- **북마크 시스템**: 관심 학과/대학 저장 및 관리 + +### 🔐 보안 시스템 +- **JWT 인증**: 안전한 토큰 기반 인증 +- **OAuth 연동**: 소셜 로그인 지원 +- **Redis 세션**: 사용자 세션 정보 캐싱 +- **권한 관리**: 역할 기반 접근 제어 + +### 💾 데이터 관리 +- **자동 타임스탬프**: 생성/수정 시간 자동 기록 +- **관계 매핑**: 사용자-검사 결과 간 효율적인 연관 관계 +- **배치 처리**: 대량 데이터 처리 최적화 + +## 🏗️ 기술 스택 + +### 🔧 Backend Framework +- **FastAPI 0.116.1**: 현대적이고 빠른 Python 웹 프레임워크 +- **Python 3.8+**: 타입 힌트와 최신 Python 기능 활용 +- **Uvicorn 0.27.1**: ASGI 서버로 고성능 비동기 처리 +- **uvloop 0.21.0**: 이벤트 루프 최적화 + +### 🗄️ Database & ORM +- **SQLAlchemy 2.0.41**: 최신 ORM으로 타입 안전성 보장 +- **MySQL**: 안정적인 관계형 데이터베이스 +- **PyMySQL 1.1.1**: Python MySQL 드라이버 +- **Redis 6.2.0**: 고성능 인메모리 캐시 + +### 🤖 AI +- **OpenAI GPT-4o**: 최신 AI 모델로 정확한 분석 +- **PyMuPDF 1.26.3**: PDF 텍스트 추출 및 처리 +- **NumPy 2.3.2**: 수치 계산 및 데이터 처리 +- **Pandas 2.3.1**: 데이터 분석 및 조작 -### 🔧 Backend -- **Framework**: FastAPI 0.116.1 -- **Language**: Python 3.8+ -- **ORM**: SQLAlchemy 2.0.41 -- **Database**: MySQL (PyMySQL 1.1.1) -- **Authentication**: JWT + OAuth -- **Cache**: Redis 6.2.0 +### ☁️ Cloud & Storage +- **AWS S3**: 확장 가능한 클라우드 스토리지 +- **boto3 1.34.44**: AWS SDK for Python +- **python-dotenv 1.1.1**: 환경 변수 관리 -### 🚀 AI & ML -- **AI Engine**: OpenAI GPT-4o -- **PDF Processing**: PyMuPDF (fitz) 1.26.3 -- **Data Analysis**: Custom scoring algorithms +### 🔒 Security & Validation +- **PyJWT 2.8.0**: JWT 토큰 생성 및 검증 +- **Pydantic 2.11.7**: 데이터 검증 및 직렬화 +- **python-multipart 0.0.20**: 파일 업로드 처리 -### ☁️ Cloud & Storage -- **Storage**: AWS S3 (boto3 1.34.44) -- **File Handling**: FastAPI UploadFile -- **Environment**: python-dotenv 1.1.1 +### 🧪 Testing & Development +- **pytest 8.4.1**: Python 테스트 프레임워크 +- **Flask 3.1.1**: 개발용 마이크로 프레임워크 +- **watchfiles 1.1.0**: 파일 변경 감지 -### 🔒 Security & Performance -- **CORS**: FastAPI CORS Middleware -- **Validation**: Pydantic 2.11.7 -- **Async Support**: uvicorn 0.27.1 + uvloop 0.21.0 -- **Testing**: pytest 8.4.1 +## 📁 프로젝트 구조 + +``` +AI/ +├── 📁 app/ # 메인 애플리케이션 +│ ├── 🚀 run.py # FastAPI 서버 실행 파일 +│ ├── 🗄️ db.py # 데이터베이스 연결 설정 +│ ├── 📁 domain/ # 도메인 모델 (엔티티) +│ │ ├── 📁 entity/ +│ │ │ └── 🏗️ BaseEntity.py # 기본 엔티티 (자동 타임스탬프) +│ │ ├── 👤 User.py # 사용자 엔티티 +│ │ ├── 🧠 Hmt.py # 흥미검사 엔티티 +│ │ ├── 🎯 Cst.py # 직업적성검사 엔티티 +│ │ ├── 🤖 AiReport.py # AI 분석 리포트 엔티티 +│ │ ├── 📁 reportModule/ # 리포트 모듈 +│ │ │ ├── 📊 Report.py # 리포트 엔티티 +│ │ │ └── 📈 ReportScore.py # 리포트 점수 엔티티 +│ │ ├── 📁 mockModule/ # 모의고사 모듈 +│ │ │ ├── 📝 Mock.py # 모의고사 엔티티 +│ │ │ └── 🎯 MockScore.py # 모의고사 점수 엔티티 +│ │ ├── 🎓 Major.py # 전공 엔티티 +│ │ ├── 🌟 Field.py # 분야 엔티티 +│ │ ├── 🏫 University.py # 대학교 엔티티 +│ │ ├── 🔗 UniversityMajor.py # 대학-전공 연관 엔티티 +│ │ ├── 🔐 OAuth.py # OAuth 인증 엔티티 +│ │ ├── 📊 AdmissionScore.py # 입학 성적 엔티티 +│ │ └── 🔖 MajorBookmark.py # 학과 북마크 엔티티 +│ ├── 📁 DTO/ # 데이터 전송 객체 +│ │ ├── 🧠 HmtDTO.py # 흥미검사 DTO +│ │ ├── 🎯 CstDTO.py # 직업적성검사 DTO +│ │ ├── 👤 UserDTO.py # 사용자 DTO +│ │ └── 🤖 AiRepotDto.py # AI 리포트 DTO +│ ├── 📁 repository/ # 데이터 접근 계층 +│ │ ├── 🏗️ Repository.py # 기본 레포지토리 (CRUD 공통 로직) +│ │ ├── 👤 userRepository.py # 사용자 레포지토리 +│ │ ├── 🧠 hmtRepository.py # 흥미검사 레포지토리 +│ │ ├── 🎯 cstRepository.py # 직업적성검사 레포지토리 +│ │ ├── 🤖 aiReportRepository.py # AI 리포트 레포지토리 +│ │ ├── 📊 reportRepository.py # 리포트 레포지토리 +│ │ ├── 📝 mockRepository.py # 모의고사 레포지토리 +│ │ ├── 🎓 majorRepository.py # 전공 레포지토리 +│ │ ├── 🌟 fieldRepository.py # 분야 레포지토리 +│ │ ├── 🏫 universityRepository.py # 대학교 레포지토리 +│ │ ├── 🔐 oauthRepository.py # OAuth 레포지토리 +│ │ └── 📊 admissionScoreRepository.py # 입학 성적 레포지토리 +│ ├── 📁 services/ # 비즈니스 로직 계층 +│ │ ├── 🧠 HmtService.py # 흥미검사 서비스 +│ │ ├── 🎯 CstService.py # 직업적성검사 서비스 +│ │ ├── 👤 UserService.py # 사용자 서비스 +│ │ ├── 🤖 AiReportService.py # AI 리포트 서비스 +│ │ └── 📊 ExcelMappingService.py # Excel 매핑 서비스 +│ ├── 📁 routes/ # API 라우터 (컨트롤러) +│ │ ├── 🧠 HmtController.py # 흥미검사 컨트롤러 +│ │ ├── 🎯 CstController.py # 직업적성검사 컨트롤러 +│ │ ├── 🔐 AuthController.py # 인증 컨트롤러 +│ │ └── 🤖 AiReportController.py # AI 리포트 컨트롤러 +│ ├── 📁 login/ # 인증 시스템 +│ │ ├── 🔐 jwt_auth.py # JWT 인증 서비스 +│ │ ├── 🛠️ jwt_util.py # JWT 유틸리티 +│ │ ├── 🌐 oauth_jwt_auth.py # OAuth + JWT 통합 인증 +│ │ ├── 🗄️ redis_auth.py # Redis 인증 +│ │ └── 👤 jwtUser.py # JWT 사용자 모델 +│ ├── 📁 gptApi/ # GPT API 연동 +│ │ ├── 🧠 gptEngine.py # GPT 엔진 (추상 클래스) +│ │ ├── 📊 testReportEng/ # 테스트 리포트 엔진 +│ │ │ └── 📈 testReport.py # 테스트 리포트 생성 +│ │ └── 🧪 test.py # GPT API 테스트 +│ ├── 📁 util/ # 유틸리티 +│ │ ├── 🔄 Transactional.py # 트랜잭션 데코레이터 (읽기/쓰기 분리) +│ │ ├── 📁 globalDB/ # 글로벌 DB 컨텍스트 +│ │ │ ├── 🔗 db_context.py # DB 컨텍스트 관리 +│ │ │ └── 🌐 global_db.py # 글로벌 DB 접근 +│ │ ├── 📁 PdfExtracter/ # PDF 처리 엔진 +│ │ │ ├── 🧠 HmtExtracter.py # 흥미검사 PDF 분석 +│ │ │ └── 🎯 CstExtracter.py # 직업적성검사 PDF 분석 +│ │ ├── 📁 S3/ # AWS S3 연동 +│ │ │ └── ☁️ S3_Util.py # S3 파일 업로드/다운로드 +│ │ ├── 🔐 auth_dependency.py # 인증 의존성 +│ │ ├── 📅 termGenerator.py # 학기 생성기 +│ │ ├── 🔍 similarity_checker.py # 유사도 검사 유틸리티 +│ │ └── 📊 GradeComparisonUtil.py # 성적 비교 유틸리티 +│ ├── 📁 globals/ # 전역 설정 +│ │ ├── 🚨 exceptions.py # 커스텀 예외 및 에러코드 +│ │ ├── 🛡️ exception_handler.py # 전역 예외 처리기 +│ │ └── 📋 error_codes.md # 에러코드 문서 +│ ├── 📁 mappingProgram/ # 데이터 매핑 프로그램 +│ │ ├── 🔄 remap_with_similarity.py # 유사도 기반 재매핑 +│ │ ├── 📊 show_db_data.py # DB 데이터 조회 +│ │ └── 🔍 show_unmapped_combinations.py # 미매핑 조합 조회 +│ ├── 🗄️ redisClient.py # Redis 클라이언트 +│ ├── 📊 교과 데이터.csv # 입학 성적 CSV 데이터 +│ ├── 📈 입학성적_전체데이터.csv # 전체 입학 성적 데이터 +│ └── __init__.py +├── 📁 Test/ # 테스트 코드 +├── 📁 PdfExtractor/ # PDF 추출 테스트 +├── 🧪 pytest.ini # pytest 설정 +├── 📦 requirements.txt # Python 의존성 +├── 🚀 ai-api.service # 시스템 서비스 파일 +└── 📖 README.md # 프로젝트 문서 +``` ## 🚀 시작하기 @@ -153,37 +233,39 @@ pip install -r requirements.txt `.env` 파일을 생성하고 다음 변수들을 설정하세요: ```env -GPT_API_KEY= +# GPT API 설정 +GPT_API_KEY=your_openai_api_key_here +# 데이터베이스 선택 (LOCAL 또는 AWS) DB_SELECT=LOCAL -#LOCAL -LOCAL_HOST= -LOCAL_USER= -LOCAL_PASSWORD= -LOCAL_PORT= -LOCAL_NAME= - -#AWS -AWS_USER= -AWS_PASSWORD= -AWS_ADDRESS= -AWS_NAME= -AWS_PORT= - -#S3 -AWS_S3_BUCKET_NAME= -AWS_S3_ACCESS_KEY= -AWS_S3_SECRET_KEY= - -#JWT -JWT_SECRET_KEY= -JWT_EXPIRATION=3600 -JWT_REFRESH_EXPIRATION=86400 - -#Radis -REDIS_HOST= -REDIS_PORT= +# 로컬 데이터베이스 설정 +LOCAL_HOST=localhost +LOCAL_USER=your_username +LOCAL_PASSWORD=your_password +LOCAL_PORT=3306 +LOCAL_NAME=your_database_name + +# AWS RDS 데이터베이스 설정 +AWS_USER=your_aws_username +AWS_PASSWORD=your_aws_password +AWS_ADDRESS=your_aws_endpoint +AWS_NAME=your_aws_database_name +AWS_PORT=3306 + +# AWS S3 설정 +AWS_S3_BUCKET_NAME=your_s3_bucket_name +AWS_S3_ACCESS_KEY=your_s3_access_key +AWS_S3_SECRET_KEY=your_s3_secret_key + +# JWT 설정 +JWT_SECRET_KEY=your_jwt_secret_key_here +JWT_EXPIRATION=3600 # 액세스 토큰 만료 시간 (초) +JWT_REFRESH_EXPIRATION=86400 # 리프레시 토큰 만료 시간 (초) + +# Redis 설정 +REDIS_HOST=localhost +REDIS_PORT=6379 ``` ### 3. 데이터베이스 설정 @@ -294,6 +376,14 @@ python run.py #### DELETE /cst/{cst_id} 직업적성검사를 삭제합니다. +### 🤖 AI 리포트 API + +#### POST /ai-report +검사 결과를 기반으로 AI 분석 리포트를 생성합니다. + +#### GET /ai-report/my +현재 사용자의 모든 AI 리포트를 조회합니다. + ### 🧪 테스트 API #### GET /test-error @@ -302,7 +392,7 @@ python run.py #### GET /health 서버 상태 확인 -## 🔧 핵심 기능 +## 🏗️ 핵심 아키텍처 ### 📄 PDF 처리 엔진 @@ -321,12 +411,29 @@ python run.py #### GptBase (추상 클래스) - **모델**: GPT-4o - **응답 형식**: JSON 강제 -- **토큰 제한**: 3000 +- **토큰 제한**: 10000 - **온도**: 0.6 (창의성과 일관성의 균형) #### 구현체 - **TestReportEngine**: 테스트 결과 기반 맞춤형 리포트 생성 +### 🎓 입학 성적 분석 시스템 + +#### ExcelMappingService +- **CSV 자동 매핑**: 입학 성적 데이터를 대학교/학과와 자동 연결 +- **유사도 기반 매칭**: 대학교명과 학과명의 유사도를 통한매칭 +- **데이터 검증**: 성적 데이터의 유효성 검사 및 보정 + +#### SimilarityChecker +- **대학교명 매칭**: 별칭, 캠퍼스명 등을 고려한 매칭 +- **학과명 매칭**: 유사도 기반 퍼지 매칭 +- **별칭 사전**: 주요 대학교의 일반적인 별칭 관리 + +#### GradeComparisonUtil +- **교과 성적 분석**: 사용자별 과목별 평균 등급 계산 +- **입학 가능성 분석**: 사용자 성적과 입학 컷 비교 +- **북마크 연동**: 관심 학과/대학과의 성적 비교 + ### 🔐 인증 시스템 #### JWT + OAuth 통합 @@ -348,6 +455,9 @@ python run.py - **User ↔ Hmt**: 1:N (사용자당 여러 흥미검사) - **User ↔ Cst**: 1:N (사용자당 여러 직업적성검사) - **User ↔ AiReport**: 1:N (사용자당 여러 AI 리포트) +- **User ↔ Report**: 1:N (사용자당 여러 교과 성적) +- **User ↔ MajorBookmark**: 1:N (사용자당 여러 학과 북마크) +- **University ↔ Major**: M:N (대학교-학과 다대다 관계) ### 🔄 트랜잭션 관리 @@ -356,39 +466,19 @@ python run.py - **@TransactionalRead**: 읽기 전용 (커밋 없음) - **@TransactionalWrite**: 쓰기 전용 (명시적 커밋) -#### 성능 최적화 -- **배치 처리**: `save_all()` 메서드로 대량 데이터 처리 -- **연결 풀**: 최적화된 DB 연결 관리 - -## 🚨 에러 처리 -*: OAuth 테이블 → User 테이블 연쇄 조회 -- **권한 관리**: authorities 기반 접근 제어 - -#### Redis 연동 -- **세션 관리**: 사용자 세션 정보 캐싱 -- **토큰 저장**: 리프레시 토큰 관리 - -### 💾 데이터베이스 설계 - -#### BaseEntity -- **자동 타임스탬프**: `created_at`, `updated_at` -- **테이블명 자동화**: 클래스명 기반 snake_case 변환 +## ⚡ 성능 최적화 -#### 관계 설정 -- **User ↔ Hmt**: 1:N (사용자당 여러 흥미검사) -- **User ↔ Cst**: 1:N (사용자당 여러 직업적성검사) -- **User ↔ AiReport**: 1:N (사용자당 여러 AI 리포트) +### 🚀 데이터베이스 최적화 -### 🔄 트랜잭션 관리 +#### Repository 패턴 개선 +- **flush_immediately 옵션**: 선택적 DB 동기화로 성능 향상 +- **save_all() 메서드**: 배치 삽입으로 대량 데이터 처리 최적화 +- **연결 풀 최적화**: pool_size=20, max_overflow=30 설정 -#### Transactional 데코레이터 -- **@Transactional**: 기본 트랜잭션 (자동 커밋) -- **@TransactionalRead**: 읽기 전용 (커밋 없음) -- **@TransactionalWrite**: 쓰기 전용 (명시적 커밋) +#### 트랜잭션 최적화 +- **읽기/쓰기 분리**: 불필요한 커밋 오버헤드 제거 +- **배치 처리**: 대량 데이터 처리 시 성능 향상 -#### 성능 최적화 -- **배치 처리**: `save_all()` 메서드로 대량 데이터 처리 -- **연결 풀**: 최적화된 DB 연결 관리 ## 🚨 에러 처리 @@ -418,33 +508,6 @@ class ErrorCode(Enum): ### 에러 전파 체계 -``` -### 체계적 에러코드 시스템 - -```python -class ErrorCode(Enum): - # 공통 에러 (1000-1999) - UNKNOWN_ERROR = (1000, "알 수 없는 오류가 발생했습니다.") - VALIDATION_ERROR = (1005, "입력 데이터가 올바르지 않습니다.") - - # 사용자 관련 에러 (2000-2999) - USER_NOT_FOUND = (2000, "사용자를 찾을 수 없습니다.") - - # 파일 관련 에러 (3000-3999) - FILE_TYPE_NOT_SUPPORTED = (3002, "지원하지 않는 파일 형식입니다.") - - # 검사 관련 에러 (4000-4999) - HMT_PROCESSING_ERROR = (4001, "흥미검사 처리 중 오류가 발생했습니다.") - CST_PROCESSING_ERROR = (4101, "직업적성검사 처리 중 오류가 발생했습니다.") - - # 외부 서비스 관련 에러 (6000-6999) - S3_UPLOAD_ERROR = (6000, "파일 업로드 서비스 오류가 발생했습니다.") - PDF_PROCESSING_ERROR = (6001, "PDF 처리 중 오류가 발생했습니다.") - AI_PROCESSING_ERROR = (6002, "AI 분석 처리 중 오류가 발생했습니다.") -``` - -### 에러 전파 체계 - ``` PDF 처리 (PdfExtracter) → 비즈니스 로직 (Service) → API (Controller) → 클라이언트 ``` @@ -475,12 +538,49 @@ python -m pytest test_Cst.py # 상세 출력 python -m pytest -v + +# 커버리지 확인 +python -m pytest --cov=app ``` +### 테스트 구조 +- **단위 테스트**: 개별 함수/클래스 테스트 +- **통합 테스트**: API 엔드포인트 테스트 +- **PDF 처리 테스트**: 파일 업로드 및 분석 테스트 + +### 환경별 설정 + +#### 개발 환경 +- `DB_SELECT=LOCAL` +- `echo=True` (SQL 로깅) +- `debug=True` + +#### 프로덕션 환경 +- `DB_SELECT=AWS` +- `echo=False` +- `debug=False` +- 환경 변수 보안 강화 + +## 🤝 기여하기 + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + ## 📄 라이선스 This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +## 📞 문의 + +프로젝트에 대한 문의사항이 있으시면 이슈를 생성해 주세요. + +--- + +**⭐ 이 프로젝트가 도움이 되었다면 스타를 눌러주세요!** + diff --git a/ai-api.service b/ai-api.service new file mode 100644 index 0000000..0011d8a --- /dev/null +++ b/ai-api.service @@ -0,0 +1,26 @@ +[Unit] +Description=AI API Gunicorn Service +After=network.target + +[Service] +Type=notify +User=ubuntu +Group=ubuntu +WorkingDirectory=/home/ubuntu/AI +Environment="PATH=/home/ubuntu/AI/venv/bin" +ExecStart=/home/ubuntu/AI/venv/bin/gunicorn -c gunicorn.conf.py run:app +ExecReload=/bin/kill -s HUP $MAINPID +KillMode=mixed +TimeoutStopSec=5 +PrivateTmp=true +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target + + + + + + diff --git a/app/DTO/AiRepotDto.py b/app/DTO/AiRepotDto.py index 7acd730..3396faa 100644 --- a/app/DTO/AiRepotDto.py +++ b/app/DTO/AiRepotDto.py @@ -1,6 +1,7 @@ from pydantic import BaseModel,ConfigDict, Field, field_validator from datetime import datetime from typing import Optional, Any +import json class AiReportListResponse(BaseModel): id: int @@ -16,11 +17,11 @@ class AiReportResponse(BaseModel): userName:Optional[str]=None created_at:datetime CstID:int - HmtID:Optional[int]=None - testReport:Optional[str]=None - scoreReport:Optional[str]=None - majorReport:Optional[str]=None - totalReport:Optional[str]=None + HmtID:int + testReport:Optional[dict]=None + scoreReport:Optional[dict]=None + majorReport:Optional[dict]=None + totalReport:Optional[dict]=None model_config = ConfigDict(from_attributes=True) @@ -32,6 +33,62 @@ def get_user_name(cls, v, info): return getattr(info.data.user, 'name', None) return v + @field_validator('testReport', mode='before') + @classmethod + def parse_test_report(cls, v, info): + """testReport 문자열을 JSON으로 파싱합니다.""" + if isinstance(v, str): + # 'None' 문자열이나 빈 문자열은 None으로 처리 + if v.lower() in ['none', 'null', ''] or v.strip() == '': + return None + try: + return json.loads(v) + except json.JSONDecodeError: + return v + return v + + @field_validator('scoreReport', mode='before') + @classmethod + def parse_score_report(cls, v, info): + """scoreReport 문자열을 JSON으로 파싱합니다.""" + if isinstance(v, str): + # 'None' 문자열이나 빈 문자열은 None으로 처리 + if v.lower() in ['none', 'null', ''] or v.strip() == '': + return None + try: + return json.loads(v) + except json.JSONDecodeError: + return v + return v + + @field_validator('majorReport', mode='before') + @classmethod + def parse_major_report(cls, v, info): + """majorReport 문자열을 JSON으로 파싱합니다.""" + if isinstance(v, str): + # 'None' 문자열이나 빈 문자열은 None으로 처리 + if v.lower() in ['none', 'null', ''] or v.strip() == '': + return None + try: + return json.loads(v) + except json.JSONDecodeError: + return v + return v + + @field_validator('totalReport', mode='before') + @classmethod + def parse_total_report(cls, v, info): + """totalReport 문자열을 JSON으로 파싱합니다.""" + if isinstance(v, str): + # 'None' 문자열이나 빈 문자열은 None으로 처리 + if v.lower() in ['none', 'null', ''] or v.strip() == '': + return None + try: + return json.loads(v) + except json.JSONDecodeError: + return v + return v + class AiReportRequest(BaseModel): reportGradeNum: int=Field(description='레포트 생성할 학년(반영 성적 및 추천 내용 달라짐)') reportTermNum: int=Field(description='레포트 생성할 학기(반영 성적 및 추천 내용 달라짐)') diff --git a/app/domain/MajorBookmark.py b/app/domain/MajorBookmark.py index 5ffc9a5..4c679d3 100644 --- a/app/domain/MajorBookmark.py +++ b/app/domain/MajorBookmark.py @@ -3,7 +3,7 @@ from sqlalchemy import Column, Integer, String, ForeignKey, BigInteger class MajorBookmark(BaseEntity): - __tablename__ = "mahor_bookmark" + __tablename__ = "major_bookmark" __table_args__ = { "mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", diff --git a/app/domain/reportModule/Report.py b/app/domain/reportModule/Report.py index 8daafd4..1ac06df 100644 --- a/app/domain/reportModule/Report.py +++ b/app/domain/reportModule/Report.py @@ -18,4 +18,4 @@ class Report(BaseEntity): userGrade=Column(Integer,name='user_grade',nullable=False)#유저 학년 user=relationship("User",back_populates="reports") - reportScores=relationship("ReportScore",back_populates="report") + reportScores=relationship("ReportScore", back_populates="report") diff --git a/app/domain/reportModule/ReportScore.py b/app/domain/reportModule/ReportScore.py index 7101f75..3ac2d58 100644 --- a/app/domain/reportModule/ReportScore.py +++ b/app/domain/reportModule/ReportScore.py @@ -22,7 +22,7 @@ class ReportScore(BaseEntity): score=Column(Integer,name='score',nullable=True) #원점수 credit=Column(Integer,name='credit',nullable=False) #학점 - rId=Column(BigInteger,ForeignKey('report.r_id'),name='r_id',nullable=False) + rId=Column(BigInteger, ForeignKey('report.r_id'), name='r_id', nullable=False) report=relationship("Report", back_populates="reportScores") diff --git a/app/gptApi/testReport.py b/app/gptApi/testReport.py index ecf45f4..8121d8c 100644 --- a/app/gptApi/testReport.py +++ b/app/gptApi/testReport.py @@ -149,7 +149,7 @@ def output_constructor() -> str: return """ 다음과 같은 json 형태로 출력해줘 { - cst_hmt_test_report: 3000자 내외 택스트 형태로 레포트 결과물 + content: 3000자 내외 택스트 형태로 레포트 결과물 major: 추천 학과 리스트 field: 추천 계열 리스트 hmt: 흥미 유형 상위 2개 영역 리스트 (예시: ["1순위 R유형","2순위 S유형"]) diff --git a/app/gptApi/totalReport.py b/app/gptApi/totalReport.py index b3cb14b..963b45a 100644 --- a/app/gptApi/totalReport.py +++ b/app/gptApi/totalReport.py @@ -96,7 +96,7 @@ def output_constructor() ->str: prompt = """ 다음과 같은 json으로 출력해줘 { - content: 출력결과 + content: 출력결과 4000자내외 } """ return prompt diff --git a/app/repository/majorBookmarkRepository.py b/app/repository/majorBookmarkRepository.py new file mode 100644 index 0000000..01d9be9 --- /dev/null +++ b/app/repository/majorBookmarkRepository.py @@ -0,0 +1,26 @@ +from domain.MajorBookmark import MajorBookmark +from .Repository import BaseRepository +from typing import Optional, List +from sqlalchemy.orm import joinedload + +class MajorBookmarkRepository(BaseRepository[MajorBookmark]): + def __init__(self): + super().__init__(model=MajorBookmark) + + def getByUserId(self, uid: str) -> List[MajorBookmark]: + """사용자 ID로 북마크된 학과-학교 목록을 조회합니다.""" + return (self.session.query(self.model) + .options(joinedload(self.model.major), joinedload(self.model.university)) + .filter(self.model.uid == uid) + .all()) + + def getByUserIdWithUniversityAndMajor(self, uid: str) -> List[MajorBookmark]: + """사용자 ID로 대학교와 학과 정보가 모두 있는 북마크만 조회합니다.""" + return (self.session.query(self.model) + .options(joinedload(self.model.major), joinedload(self.model.university)) + .filter(self.model.uid == uid) + .filter(self.model.univId.isnot(None)) + .filter(self.model.major_id.isnot(None)) + .all()) + +majorBookmarkRepository = MajorBookmarkRepository() diff --git a/app/routes/AdmissionPossibilityController.py b/app/routes/AdmissionPossibilityController.py new file mode 100644 index 0000000..fe3e997 --- /dev/null +++ b/app/routes/AdmissionPossibilityController.py @@ -0,0 +1,116 @@ +from fastapi import APIRouter, Depends +from typing import Dict +from services.AdmissionPossibilityService import admissionPossibilityService +from login.oauth_jwt_auth import get_current_user +from domain.User import User +from globals import create_success_response, ErrorCode, raise_business_exception + +router = APIRouter(prefix="/api/admission-possibility", tags=["합격가능성"]) + +@router.get("/my", summary="내 합격가능성 조회") +async def get_my_admission_possibility( + current_user: User = Depends(get_current_user) +) -> Dict: + """ + 현재 로그인된 사용자의 북마크된 학과-학교에 대한 합격가능성을 분석합니다. + + Returns: + Dict: 합격가능성 분석 결과 + """ + try: + result = admissionPossibilityService.getUserAdmissionPossibility(current_user.uid) + + if "error" in result: + raise_business_exception( + ErrorCode.UNKNOWN_ERROR, + result["error"] + ) + + return create_success_response( + result, + "합격가능성 분석이 완료되었습니다." + ) + + except Exception as e: + raise_business_exception( + ErrorCode.UNKNOWN_ERROR, + f"합격가능성 분석 중 오류가 발생했습니다: {str(e)}" + ) + +@router.get("/bookmark/{bookmark_id}", summary="특정 북마크 합격가능성 조회") +async def get_bookmark_possibility( + bookmark_id: int, + current_user: User = Depends(get_current_user) +) -> Dict: + """ + 특정 북마크에 대한 합격가능성을 분석합니다. + + Args: + bookmark_id: 북마크 ID + + Returns: + Dict: 특정 북마크의 합격가능성 분석 결과 + """ + try: + result = admissionPossibilityService.getSpecificBookmarkPossibility(current_user.uid, bookmark_id) + + if "error" in result: + raise_business_exception( + ErrorCode.UNKNOWN_ERROR, + result["error"] + ) + + return create_success_response( + result, + "북마크 합격가능성 분석이 완료되었습니다." + ) + + except Exception as e: + raise_business_exception( + ErrorCode.UNKNOWN_ERROR, + f"북마크 분석 중 오류가 발생했습니다: {str(e)}" + ) + +@router.get("/summary", summary="내 합격가능성 요약 조회") +async def get_admission_possibility_summary( + current_user: User = Depends(get_current_user) +) -> Dict: + """ + 현재 로그인된 사용자의 합격가능성 요약 정보를 제공합니다. + + Returns: + Dict: 합격가능성 요약 정보 + """ + try: + result = admissionPossibilityService.getUserAdmissionPossibility(current_user.uid) + + if "error" in result: + raise_business_exception( + ErrorCode.UNKNOWN_ERROR, + result["error"] + ) + + # 요약 정보만 추출 + if "analysis" in result and result["analysis"]: + summary = result["analysis"].get("summary", {}) + return create_success_response( + { + "uid": current_user.uid, + "summary": summary + }, + "합격가능성 요약 조회가 완료되었습니다." + ) + else: + return create_success_response( + { + "uid": current_user.uid, + "summary": {"message": "분석할 북마크가 없습니다."} + }, + "북마크가 없습니다." + ) + + except Exception as e: + raise_business_exception( + ErrorCode.UNKNOWN_ERROR, + f"합격가능성 요약 조회 중 오류가 발생했습니다: {str(e)}" + ) diff --git a/app/routes/AiReportController.py b/app/routes/AiReportController.py index ef1a25b..f9f1af8 100644 --- a/app/routes/AiReportController.py +++ b/app/routes/AiReportController.py @@ -16,7 +16,7 @@ router =APIRouter(prefix='/aireport') -@router.post("/me",summary="현재 유저의 모든 aiReport를 가져옵니다.") +@router.get("/me",summary="현재 유저의 모든 aiReport를 가져옵니다.") async def getAiReportsByMe(current_user:User = Depends(get_current_user)): try: result_list:List[AiReportListResponse]= aiReportService.getAllAiReportsByUser(current_user.uid) diff --git a/app/run.py b/app/run.py index 0e35f46..3705293 100644 --- a/app/run.py +++ b/app/run.py @@ -4,6 +4,7 @@ from routes.CstController import router as cst_router from routes.AuthController import router as auth_router from routes.AiReportController import router as ai_report_router +from routes.AdmissionPossibilityController import router as admission_possibility_router from globals import setup_exception_handlers from util.globalDB.db_context import set_db, reset_db from db import SessionLocal,engine @@ -74,6 +75,7 @@ async def db_session_middleware(request: Request, call_next): app.include_router(cst_router, tags=["직업적성검사"]) app.include_router(ai_report_router,tags=["ai레포트"]) +app.include_router(admission_possibility_router, tags=["합격가능성"]) @app.get("/") async def root(): @@ -92,11 +94,12 @@ async def test_error(): raise_file_exception(ErrorCode.PDF_PROCESSING_ERROR, "테스트 에러 메시지입니다.") if __name__ == "__main__": - uvicorn.run( "run:app", # app 폴더 안에서 실행될 때의 경로 host="0.0.0.0", port=8081, reload=True, + ssl_keyfile="../127.0.0.1+1-key.pem", # 개인키 파일 경로 + ssl_certfile="../127.0.0.1.pem", # 인증서 파일 경로 log_level="debug" # 더 자세한 로그 ) \ No newline at end of file diff --git a/app/services/AdmissionPossibilityService.py b/app/services/AdmissionPossibilityService.py new file mode 100644 index 0000000..a0e01ab --- /dev/null +++ b/app/services/AdmissionPossibilityService.py @@ -0,0 +1,307 @@ +from typing import List, Dict, Optional +from domain.User import User +from domain.MajorBookmark import MajorBookmark +from domain.AdmissionScore import AdmissionScore +from repository.majorBookmarkRepository import majorBookmarkRepository +from repository.userRepository import userRepository +from util.Transactional import Transactional +from util.globalDB.global_db import get_global_db + +class AdmissionPossibilityService: + """ + 사용자의 대학 학과 북마크에 대한 합격가능성을 분석하는 서비스 + """ + + def __init__(self): + pass + + @Transactional + def getUserAdmissionPossibility(self, uid: str) -> Dict: + """ + 사용자의 북마크된 학과-학교에 대한 합격가능성을 분석합니다. + + Args: + uid: 사용자 ID + + Returns: + Dict: 합격가능성 분석 결과 + """ + try: + # 사용자 정보 조회 + user = userRepository.getById(uid) + if not user: + return {"error": "사용자를 찾을 수 없습니다."} + + # 대학교와 학과 정보가 모두 있는 북마크만 조회 + bookmarks = majorBookmarkRepository.getByUserIdWithUniversityAndMajor(uid) + if not bookmarks: + return { + "message": "등록된 대학-학과 북마크가 없습니다.", + "uid": uid, + "bookmarks": [], + "analysis": None + } + + # 합격가능성 분석 + analysis_result = self._analyzeAdmissionPossibility(user, bookmarks) + + bookmark_results = [] + for bookmark in bookmarks: + analysis = self._analyzeSingleBookmark(user, bookmark) + + bookmark_results.append({ + "name": f"{bookmark.major.name}-{bookmark.university.name}", + "percent": -1 if analysis.get("level") == "unknown" else (100 if analysis.get("level") == "high" else (70 if analysis.get("level") == "medium" else 30)), + "grade_diff": analysis.get("diff"), + "user_grade": analysis.get("user_grade", 0), + "cutoff_grade": analysis.get("cutoff_90") + }) + + return { + "user_grade": analysis_result.get("user_overall_grade"), + "bookmarks": len(bookmarks), + "bookmark_results": bookmark_results + } + + except Exception as e: + return {"error": f"합격가능성 분석 중 오류가 발생했습니다: {str(e)}"} + + @Transactional + def getSpecificBookmarkPossibility(self, uid: str, bookmark_id: int) -> Dict: + """ + 특정 북마크에 대한 합격가능성을 분석합니다. + + Args: + uid: 사용자 ID + bookmark_id: 북마크 ID + + Returns: + Dict: 특정 북마크의 합격가능성 분석 결과 + """ + try: + # 사용자 정보 조회 + user = userRepository.getById(uid) + if not user: + return {"error": "사용자를 찾을 수 없습니다."} + + # 특정 북마크 조회 + bookmark = majorBookmarkRepository.getById(bookmark_id) + if not bookmark or bookmark.uid != uid: + return {"error": "북마크를 찾을 수 없거나 접근 권한이 없습니다."} + + if not bookmark.univId or not bookmark.major_id: + return {"error": "대학교 또는 학과 정보가 없는 북마크입니다."} + + # 해당 북마크만 분석 + analysis_result = self._analyzeSingleBookmark(user, bookmark) + + return { + "uid": uid, + "bookmark_id": bookmark_id, + "bookmark": self._formatSingleBookmark(bookmark), + "analysis": analysis_result + } + + except Exception as e: + return {"error": f"북마크 분석 중 오류가 발생했습니다: {str(e)}"} + + def _analyzeAdmissionPossibility(self, user: User, bookmarks: List[MajorBookmark]) -> Dict: + """북마크 목록에 대한 합격가능성을 종합 분석합니다.""" + try: + # 사용자 전체 평균 등급 (임시로 고정값 사용) + user_overall_grade = 3.0 # 실제로는 성적 데이터에서 계산해야 함 + + # 각 북마크별 분석 결과 + bookmark_analyses = [] + total_possibility = { + "high": 0, # 높음 (하향 지원) + "medium": 0, # 보통 (적정 지원) + "low": 0, # 낮음 (상향 지원) + "unknown": 0 # 알 수 없음 + } + + for bookmark in bookmarks: + analysis = self._analyzeSingleBookmark(user, bookmark) + bookmark_analyses.append(analysis) + + # 합격가능성 분류 + if analysis.get("level") == "high": + total_possibility["high"] += 1 + elif analysis.get("level") == "medium": + total_possibility["medium"] += 1 + elif analysis.get("level") == "low": + total_possibility["low"] += 1 + else: + total_possibility["unknown"] += 1 + + return { + "user_overall_grade": user_overall_grade, + "total_bookmarks": len(bookmarks), + "total_possibility": total_possibility, + "bookmark_analyses": bookmark_analyses, + "summary": self._generateSummary(total_possibility, user_overall_grade) + } + + except Exception as e: + return {"error": f"종합 분석 중 오류 발생: {str(e)}"} + + def _analyzeSingleBookmark(self, user: User, bookmark: MajorBookmark) -> Dict: + """단일 북마크에 대한 합격가능성을 분석합니다.""" + try: + # 사용자 전체 평균 등급 (임시로 고정값 사용) + user_overall_grade = 3.0 # 실제로는 성적 데이터에서 계산해야 함 + + # 합격 등급컷 조회 + admission_scores = self._getAdmissionScores(bookmark.univId, bookmark.major_id) + + if not admission_scores: + return { + "possibility_level": "unknown", + "reason": "합격 등급컷 데이터가 없습니다.", + "user_grade": user_overall_grade, + "cutoff_data": None + } + + # 가장 낮은 90% 합격 등급컷 찾기 (가장 엄격한 기준) + best_cutoff = None + best_admission_type = None + + for score in admission_scores: + if score.cutNinety and score.cutNinety > 0: + if best_cutoff is None or score.cutNinety < best_cutoff: + best_cutoff = score.cutNinety + best_admission_type = score.admissionType + + if best_cutoff is None: + return { + "possibility_level": "unknown", + "reason": "유효한 90% 합격 등급컷이 없습니다.", + "user_grade": user_overall_grade, + "cutoff_data": None + } + + # 등급 비교 및 합격가능성 판단 + grade_difference = best_cutoff - user_overall_grade + + if grade_difference >= 0.5: + possibility_level = "high" + reason = "하향 지원 가능 - 안정적인 합격" + elif grade_difference >= 0: + possibility_level = "medium" + reason = "적정 지원 - 적절한 도전" + else: + possibility_level = "low" + reason = "상향 지원 - 성적 향상 필요" + + return { + "level": possibility_level, # high/medium/low + "diff": round(grade_difference, 2), # 등급 차이 + "user_grade": user_overall_grade, + "cutoff_90": best_cutoff + } + + except Exception as e: + return {"error": f"북마크 분석 중 오류 발생: {str(e)}"} + + def _getAdmissionScores(self, univ_id: int, major_id: int) -> List[AdmissionScore]: + """특정 대학교-학과의 합격 등급컷을 조회합니다.""" + try: + db_session = get_global_db() + if not db_session: + return [] + + return (db_session.query(AdmissionScore) + .filter(AdmissionScore.univId == univ_id) + .filter(AdmissionScore.majorId == major_id) + .all()) + + except Exception as e: + print(f"합격 등급컷 조회 중 오류: {e}") + return [] + + def _formatAdmissionScores(self, scores: List[AdmissionScore]) -> List[Dict]: + """합격 등급컷 데이터를 포맷팅합니다.""" + formatted = [] + for score in scores: + formatted.append({ + "admission_type": score.admissionType, + "cut_50": score.cutFifty, + "cut_70": score.cutSeventy, + "cut_90": score.cutNinety + }) + return formatted + + def _formatBookmarks(self, bookmarks: List[MajorBookmark]) -> List[Dict]: + """북마크 목록을 포맷팅합니다.""" + formatted = [] + for bookmark in bookmarks: + formatted.append({ + "id": bookmark.id, + "major_id": bookmark.major_id, + "univ_id": bookmark.univId, + "major_name": bookmark.major.name if bookmark.major else None, + "university_name": bookmark.university.name if bookmark.university else None + }) + return formatted + + def _formatSingleBookmark(self, bookmark: MajorBookmark) -> Dict: + """단일 북마크를 포맷팅합니다.""" + return { + "id": bookmark.id, + "major_id": bookmark.major_id, + "univ_id": bookmark.univId, + "major_name": bookmark.major.name if bookmark.major else None, + "university_name": bookmark.university.name if bookmark.university else None + } + + def _getUserBasicInfo(self, user: User) -> Dict: + """사용자 기본 정보를 추출합니다.""" + return { + "uid": user.uid, + "name": getattr(user, 'name', None), + "email": getattr(user, 'email', None), + "grade_num": getattr(user, 'gradeNum', None) + } + + def _generateSummary(self, total_possibility: Dict, user_grade: float) -> Dict: + """합격가능성 요약을 생성합니다.""" + total = sum(total_possibility.values()) + + if total == 0: + return {"message": "분석할 북마크가 없습니다."} + + # 합격가능성 비율 계산 + high_ratio = round((total_possibility["high"] / total) * 100, 1) + medium_ratio = round((total_possibility["medium"] / total) * 100, 1) + low_ratio = round((total_possibility["low"] / total) * 100, 1) + + # 전체적인 평가 + if high_ratio >= 50: + overall_assessment = "전체적으로 합격 가능성이 높습니다." + elif medium_ratio >= 50: + overall_assessment = "전체적으로 적정한 수준의 합격 가능성을 보입니다." + elif low_ratio >= 50: + overall_assessment = "전체적으로 합격 가능성이 낮습니다. 성적 향상이 필요합니다." + else: + overall_assessment = "다양한 수준의 합격 가능성을 보입니다." + + return { + "total_bookmarks": total, + "high_possibility": { + "count": total_possibility["high"], + "ratio": high_ratio + }, + "medium_possibility": { + "count": total_possibility["medium"], + "ratio": medium_ratio + }, + "low_possibility": { + "count": total_possibility["low"], + "ratio": low_ratio + }, + "unknown": total_possibility["unknown"], + "overall_assessment": overall_assessment, + "user_grade": user_grade + } + +admissionPossibilityService = AdmissionPossibilityService() diff --git a/app/services/AiReportService.py b/app/services/AiReportService.py index 67e2a30..230a19b 100644 --- a/app/services/AiReportService.py +++ b/app/services/AiReportService.py @@ -67,7 +67,7 @@ def getAllAiReportsByUser(self,user_id:str): @Transactional def getAireportByID(self,aiReportId:int): try: - report=self._aiReportRepository.getAiReportById(aiReportId) + report=self._aiReportRepository.getById(aiReportId) if report is None: raise BusinessException(ErrorCode.AI_REPORT_NOT_FOUND,f"AI Report with Id not found") reportResult=AiReportResponse.model_validate(report,from_attributes=True) @@ -118,14 +118,14 @@ def createAiReport(self,request:AiReportRequest,user_id:str): f"AI 리포트 생성을 위해 다음 성적 레포트가 필요합니다: {', '.join(missing_list)}" ) - cost=0 - - if len(required_reports)<=2:#test 레포트만 - cost=AiReportTokenCost.COST_OF_BEFOR_SECOND - if len(required_reports)<4: #성적레포트까지 - cost=AiReportTokenCost.COST_OF_BEFOR_THIRD - if len(required_reports)>=4:#관심 학교 학과 까지 - cost=AiReportTokenCost.COST_OF_AFTER_THIRD + # 요청된 학년-학기 조합 수에 따라 비용을 정확히 산정 + # <=2: 테스트 리포트만, <4: 성적 리포트 포함, >=4: 관심 학교/학과 포함 + if len(required_reports) <= 2: # test 레포트만 + cost = AiReportTokenCost.COST_OF_BEFOR_SECOND + elif len(required_reports) < 4: # 성적레포트까지 + cost = AiReportTokenCost.COST_OF_BEFOR_THIRD + else: # 관심 학교 학과 까지 + cost = AiReportTokenCost.COST_OF_AFTER_THIRD if cost.value>user.token: @@ -140,7 +140,7 @@ def createAiReport(self,request:AiReportRequest,user_id:str): if cost.value >= AiReportTokenCost.COST_OF_AFTER_THIRD.value: aiTotalContent=AiTotalReport(aiTestContent,aiGradeContent,user,True) else: - aiTestContent=AiTotalReport(aiTestContent,aiGradeContent,user,True) + aiTotalContent=AiTotalReport(aiTestContent,aiGradeContent,user,False) aiReport=AiReport(user=user, reportTermNum=request.reportTermNum, diff --git a/app/services/UserService.py b/app/services/UserService.py index 8a192dd..97e3806 100644 --- a/app/services/UserService.py +++ b/app/services/UserService.py @@ -86,6 +86,7 @@ def addToken(self, uid: str, token_amount: int): user = self.getUserById(uid) user.token += token_amount user.updatedAt = datetime.now() + self._userRepository.save(user) return user @Transactional @@ -98,6 +99,7 @@ def useToken(self, uid: str, token_amount: int): user.token -= token_amount user.updatedAt = datetime.now() + self._userRepository.save(user) return user @Transactional @@ -122,6 +124,7 @@ def deductTokenForService(self, uid: str, service_name: str, token_cost: int): user.token -= token_cost user.updatedAt = datetime.now() + self._userRepository.save(user) # 토큰 사용 로그를 반환할 수 있습니다 (실제 구현에서는 별도 테이블에 저장) return { @@ -138,6 +141,7 @@ def refundToken(self, uid: str, token_amount: int, reason: str = "환불"): user = self.getUserById(uid) user.token += token_amount user.updatedAt = datetime.now() + self._userRepository.save(user) return { 'user_id': uid, diff --git a/app/util/auth_dependency.py b/app/util/auth_dependency.py deleted file mode 100644 index 341b6bc..0000000 --- a/app/util/auth_dependency.py +++ /dev/null @@ -1,37 +0,0 @@ -from fastapi import Depends, HTTPException, status -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from typing import Optional -from .jwt_util import jwt_util -from repository.userRepository import userRepository -from domain.User import User - -security = HTTPBearer() - -async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> User: - """현재 로그인한 사용자 정보 반환""" - try: - # JWT 토큰에서 사용자 ID 추출 - user_id = jwt_util.get_user_id_from_token(credentials.credentials) - - # 데이터베이스에서 사용자 정보 조회 - user = userRepository.getById(user_id) - - if not user: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="사용자를 찾을 수 없습니다." - ) - - return user - - except HTTPException: - raise - except Exception as e: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="인증에 실패했습니다." - ) - -async def get_current_user_id(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str: - """현재 로그인한 사용자 ID만 반환""" - return jwt_util.get_user_id_from_token(credentials.credentials) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a06e9f4..73abd3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,8 +32,11 @@ jsonschema-specifications==2025.4.1 MarkupSafe==3.0.2 MyApplication==0.1.0 mysql-connector-python==9.2.0 +numpy==2.3.2 openai==1.96.1 packaging==25.0 +pandas==2.3.1 +pandas-stubs==2.3.0.250703 pluggy==1.6.0 protobuf==3.20.3 pydantic==2.11.7 @@ -61,6 +64,7 @@ types-awscrt==0.27.5 types-s3transfer==0.13.0 typing-inspection==0.4.1 typing_extensions==4.14.1 +tzdata==2025.2 urllib3==2.5.0 uvicorn==0.27.1 uvloop==0.21.0