포슀트

πŸ“ νŒ€ ν”„λ‘œμ νŠΈ λ‹΄λ‹Ή 파트 - 이λ ₯μ„œ 등둝

νŒ€ ν”„λ‘œμ νŠΈμ—μ„œ μ œκ°€ λ‹΄λ‹Ήν•œ 파트인 '이λ ₯μ„œ 등둝' 에 λŒ€ν•œ 상세 μ„€λͺ…μž…λ‹ˆλ‹€.

πŸ“ νŒ€ ν”„λ‘œμ νŠΈ λ‹΄λ‹Ή 파트 - 이λ ₯μ„œ 등둝

이λ ₯μ„œ 등둝 νŽ˜μ΄μ§€ λ§Œλ“€κΈ°


1. 파트 μ„€λͺ…

γ…€AI λΉ„λŒ€λ©΄ λ©΄μ ‘ μ—°μŠ΅μ„ μœ„ν•΄ focusjob μ‚¬μ΄νŠΈλ₯Ό λ°©λ¬Έν•œ μ‚¬μš©μžλ“€μ€ ν•„μˆ˜μ μœΌλ‘œ 이λ ₯μ„œλ₯Ό λ“±λ‘ν•΄μ•Όν•©λ‹ˆλ‹€.

γ…€μ΄μœ λŠ” λ“±λ‘ν•œ μ‚¬μš©μžμ˜ 이λ ₯μ„œ 정보λ₯Ό λ°”νƒ•μœΌλ‘œ AI λ©΄μ ‘ 질문이 맞좀 μƒμ„±λ˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€.

ㅀ이λ ₯μ„œ κ΄€λ ¨ νŽ˜μ΄μ§€λŠ” μ„Έκ°œμ˜ 파트둜 λ‚˜λ‰©λ‹ˆλ‹€.

  • 이λ ₯μ„œ 등둝

  • 이λ ₯μ„œ 관리

  • λ§žμΆ€λ²• 검사 및 AI첨삭

γ…€γ…€νŽ˜μ΄μ§€ λ‚΄μ—μ„œ μ‚¬μš©λ˜λŠ” μ£Όμš” κΈ°λŠ₯은 μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

[ κΈ°λŠ₯ ][ μ„€λͺ… ]
UIReact (next.js) 기반 이λ ₯μ„œ 등둝 및 관리 νŽ˜μ΄μ§€ UI 섀계 및 κ΅¬ν˜„
λ°±μ—”λ“œμ‚¬μš©μž 이λ ₯μ„œ 등둝 및 μ‚­μ œ λ°±μ—”λ“œ κΈ°λŠ₯ κ΅¬ν˜„ 및 DB섀계
λ§žμΆ€λ²• κ²€μ‚¬ν•œμŠ€νŽ (by λΆ€μ‚°λŒ€ν•™κ΅) APIμ‚¬μš© 이λ ₯μ„œ λ§žμΆ€λ²• 검사 κΈ°λŠ₯ κ΅¬ν˜„
이λ ₯μ„œ 첨삭gpt 3.5 turbo λͺ¨λΈ ν™œμš© 이λ ₯μ„œ AI 첨삭 κΈ°λŠ₯ κ΅¬ν˜„
PDFλ³€ν™˜ 및 μ €μž₯html2canvas, jspdf 라이브러리 ν™œμš© html νŽ˜μ΄μ§€ 캑쳐, pdf λ³€ν™˜ 및 DB μ €μž₯ κΈ°λŠ₯ κ΅¬ν˜„

2. μ‹œμ—° μ˜μƒ

(1) 이λ ₯μ„œ 등둝 및 관리

(2) λ§žμΆ€λ²• 검사 및 AI 첨삭


3. 둜직 및 μ£Όμš” μ½”λ“œ

전체 μ½”λ“œλŠ” νŽ˜μ΄μ§€ μƒλ‹¨μ˜ κΉƒν—ˆλΈŒ 링크λ₯Ό μ°Έκ³ ν•΄μ£Όμ„Έμš”

이λ ₯μ„œ 등둝 μœ νš¨μ„± 검사

μ•„λž˜ μ½”λ“œλŠ” μ‚¬μš©μžκ°€ μž‘μ„±ν•œ 이λ ₯μ„œ 데이터λ₯Ό κ²€μ¦ν•˜κΈ° μœ„ν•œ λ‘œμ§μž…λ‹ˆλ‹€.

이λ ₯μ„œ 제λͺ©, 인적사항 (ν”„λ‘œν•„ 이미지, 성별, μƒμ„Έμ£Όμ†Œ), μžκΈ°μ†Œκ°œ, 지원동기, ν•™λ ₯ μ„Ήμ…˜μ— λŒ€ν•œ μœ νš¨μ„±μ„ μ κ²€ν•©λ‹ˆλ‹€.

였λ₯˜κ°€ μžˆλŠ” 경우 μ‚¬μš©μžκ°€ ν•΄λ‹Ή μž…λ ₯ ν•„λ“œλ‘œ λ°”λ‘œ 이동할 수 μžˆλ„λ‘ 슀크둀 λ™μž‘μ„ μ„€μ •ν•©λ‹ˆλ‹€.




  // 1. 이λ ₯μ„œ 제λͺ© μœ νš¨μ„± 검사
  if (formData.resume_title.trim() === '') {
    setShowTitleError(true); // 제λͺ©μ΄ λΉ„μ–΄μžˆμœΌλ©΄ μ—λŸ¬ ν‘œμ‹œ
    if (!hasError) {
      firstErrorField = () => window.scrollTo(0, 0); // 첫 번째 μ—λŸ¬ ν•„λ“œλ‘œ 슀크둀 이동
    }
    hasError = true;
  }

  // 2. 인적사항 μ„Ήμ…˜ μœ νš¨μ„± 검사

  // ν”„λ‘œν•„ 이미지 확인
  if (!profileImage) {
    setProfileImageError(true); // ν”„λ‘œν•„ 이미지가 μ—†μœΌλ©΄ μ—λŸ¬ ν‘œμ‹œ
    if (!hasError) {
      firstErrorField = () => sectionsRef.personalInfo.current.scrollIntoView({ behavior: 'smooth' }); // μ—λŸ¬ ν•„λ“œλ‘œ 슀크둀
    }
    hasError = true;
  }

  // 성별 확인
  if (!formData.gender || !['male', 'female', 'other'].includes(formData.gender)) {
    setGenderError(true); // 성별이 μœ νš¨ν•˜μ§€ μ•ŠμœΌλ©΄ μ—λŸ¬ ν‘œμ‹œ
    if (!hasError) {
      firstErrorField = () => sectionsRef.personalInfo.current.scrollIntoView({ behavior: 'smooth' });
    }
    hasError = true;
  }

  // μƒμ„Έμ£Όμ†Œ 확인
  if (specificAddress.trim() === '') {
    setPostcodeError(true); // μƒμ„Έμ£Όμ†Œκ°€ λΉ„μ–΄μžˆμœΌλ©΄ μ—λŸ¬ ν‘œμ‹œ
    if (!hasError) {
      firstErrorField = () => sectionsRef.address.current.scrollIntoView({ behavior: 'smooth' });
    }
    hasError = true;
  }

  // 3. μžκΈ°μ†Œκ°œ μœ νš¨μ„± 검사
  if (selfIntroduction.trim() === '') {
    setShowSelfIntroError(true); // μžκΈ°μ†Œκ°œκ°€ λΉ„μ–΄μžˆμœΌλ©΄ μ—λŸ¬ ν‘œμ‹œ
    if (!hasError) {
      firstErrorField = () => sectionsRef.selfIntroduction.current.scrollIntoView({ behavior: 'smooth' });
    }
    hasError = true;
  }

  // 4. 지원동기 μœ νš¨μ„± 검사
  if (motivation.trim() === '') {
    setShowMotivationError(true); // 지원동기가 λΉ„μ–΄μžˆμœΌλ©΄ μ—λŸ¬ ν‘œμ‹œ
    if (!hasError) {
      firstErrorField = () => sectionsRef.motivation.current.scrollIntoView({ behavior: 'smooth' });
    }
    hasError = true;
  }

  // 5. ν•™λ ₯ μ„Ήμ…˜ μœ νš¨μ„± 검사
  const newEducationErrors = educationErrors.map((error) => ({ ...error }));
  educationFields.forEach((field, index) => {
    let fieldHasError = false;

    // 각 ꡐ윑 ν•­λͺ©μ— λŒ€ν•œ μœ νš¨μ„± 검사
    if (field.school_name.trim() === '') {
      if (!newEducationErrors[index]) newEducationErrors[index] = {}; // μ΄ˆκΈ°ν™”
      newEducationErrors[index].school_name = true;
      fieldHasError = true;
    }

    if (field.major.trim() === '') {
      if (!newEducationErrors[index]) newEducationErrors[index] = {}; // μ΄ˆκΈ°ν™”
      newEducationErrors[index].major = true;
      fieldHasError = true;
    }

    if (field.start_date === '') {
      if (!newEducationErrors[index]) newEducationErrors[index] = {}; // μ΄ˆκΈ°ν™”
      newEducationErrors[index].start_date = true;
      fieldHasError = true;
    }

    if (field.end_date === '') {
      if (!newEducationErrors[index]) newEducationErrors[index] = {}; // μ΄ˆκΈ°ν™”
      newEducationErrors[index].end_date = true;
      fieldHasError = true;
    }

    if (field.graduation_status === '') {
      if (!newEducationErrors[index]) newEducationErrors[index] = {}; // μ΄ˆκΈ°ν™”
      newEducationErrors[index].graduation_status = true;
      fieldHasError = true;
    }

    // 첫 번째 였λ₯˜ ν•„λ“œλ‘œ 슀크둀 이동 μ„€μ •
    if (fieldHasError && !hasError) {
      firstErrorField = () => sectionsRef.education.current.scrollIntoView({ behavior: 'smooth' });
      hasError = true;
    }
  });

  // ν•™λ ₯ 였λ₯˜ μƒνƒœ μ—…λ°μ΄νŠΈ
  setEducationErrors(newEducationErrors);

  // 첫 번째 μ—λŸ¬ ν•„λ“œλ‘œ 슀크둀
  if (firstErrorField) {
    firstErrorField();
  }

  // μ—λŸ¬κ°€ 있으면 μ’…λ£Œ
  if (hasError) return;

  // 였λ₯˜ μ—†μœΌλ©΄ 면제 사항 확인 및 λͺ¨λ‹¬ λ„μš°κΈ°
  checkAndSetExemptions();
  setModalContent('μž‘μ„± λ‚΄μš©μ€ PDF 파일둜 μ €μž₯λ©λ‹ˆλ‹€
이λ ₯μ„œλ₯Ό μ €μž₯ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?'); setIsModalOpen(true);
이λ ₯μ„œ 등둝 νŽ˜μ΄μ§€ 캑쳐 및 PDF λ³€ν™˜

μ•„λž˜ μ½”λ“œλŠ” 이λ ₯μ„œ 등둝 νŽ˜μ΄μ§€μ˜ λ‚΄μš©μ„ μΊ‘μ²˜ν•˜κ³ , 이λ₯Ό PDF 파일둜 λ³€ν™˜ν•˜λŠ” κΈ°λŠ₯을 κ΅¬ν˜„ν•©λ‹ˆλ‹€.

HTML μš”μ†Œλ₯Ό μΊ‘μ²˜ν•˜μ—¬ μ΄λ―Έμ§€λ‘œ λ³€ν™˜ν•œ ν›„, 이λ₯Ό PDF둜 μ‚½μž…ν•˜κ³ , μ—¬λŸ¬ νŽ˜μ΄μ§€μ— 걸쳐 좜λ ₯λ©λ‹ˆλ‹€.

      
      
      
  // 이λ ₯μ„œ λ‚΄μš©μ„ μΊ‘μ²˜ν•˜μ—¬ PDF둜 λ³€ν™˜ν•˜λŠ” κΈ°λŠ₯을 담은 ν•¨μˆ˜
  const generatePDF = async () => {
    // νŽ˜μ΄μ§€μ˜ λͺ¨λ“  λ²„νŠΌμ„ μˆ¨κΉ€ (PDF λ³€ν™˜ μ‹œ λ²„νŠΌμ„ μ œμ™Έν•œ λ‚΄μš©λ§Œ 포함)
    const buttons = document.querySelectorAll('button');
    buttons.forEach(button => button.style.display = 'none');
    
    // 'resume-content' IDλ₯Ό κ°€μ§„ μš”μ†Œλ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€ (이λ ₯μ„œ λ‚΄μš©)
    const content = document.getElementById('resume-content');
    
    // html2canvasλ₯Ό μ‚¬μš©ν•˜μ—¬ 이λ ₯μ„œ λ‚΄μš©μ˜ μŠ€ν¬λ¦°μƒ·μ„ 캑처
    const canvas = await html2canvas(content, { 
      scale: 2,  // 캑처의 해상도λ₯Ό 2배둜 μ„€μ •
      useCORS: true,  // CORS 문제λ₯Ό ν”Όν•˜κΈ° μœ„ν•΄ μ‚¬μš©
      scrollX: 0,
      scrollY: 0,
    });
    
    // μΊ‘μ²˜ν•œ 이미지λ₯Ό PNG ν˜•μ‹μœΌλ‘œ λ³€ν™˜
    const imgData = canvas.toDataURL('image/png');
    
    // jsPDFλ₯Ό μ‚¬μš©ν•˜μ—¬ μƒˆλ‘œμš΄ PDF λ¬Έμ„œλ₯Ό 생성
    const pdf = new jsPDF('p', 'mm', 'a4', true);
    
    const imgWidth = 207;  // 이미지 λ„ˆλΉ„ (A4 μš©μ§€ κΈ°μ€€)
    const pageHeight = 295;  // A4 μš©μ§€ 높이
    const imgHeight = (canvas.height * imgWidth) / canvas.width;  // 이미지 λΉ„μœ¨μ— λ§žλŠ” 높이 계산
    
    let heightLeft = imgHeight;  // 남은 νŽ˜μ΄μ§€ 높이
    let position = 0;  // 이미지가 좔가될 μœ„μΉ˜
    
    // 첫 νŽ˜μ΄μ§€μ— 이미지λ₯Ό μΆ”κ°€
    pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
    heightLeft -= pageHeight;  // 남은 λ†’μ΄μ—μ„œ ν•œ νŽ˜μ΄μ§€μ˜ 높이λ₯Ό 뺌
    
    // νŽ˜μ΄μ§€μ— 이미지가 λ‚¨μ•„μžˆμœΌλ©΄ μΆ”κ°€ νŽ˜μ΄μ§€λ₯Ό μƒμ„±ν•˜κ³  이미지λ₯Ό μΆ”κ°€
    while (heightLeft >= 0) {
      position = heightLeft - imgHeight;  // μƒˆλ‘œμš΄ νŽ˜μ΄μ§€μ— λ§žλŠ” μœ„μΉ˜ 계산
      pdf.addPage();  // μƒˆ νŽ˜μ΄μ§€ μΆ”κ°€
      pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);  // μƒˆλ‘œμš΄ νŽ˜μ΄μ§€μ— 이미지 μΆ”κ°€
      heightLeft -= pageHeight;
    }
    
    // PDF λ¬Έμ„œλ₯Ό Blob ν˜•μ‹μœΌλ‘œ λ°˜ν™˜
    const pdfBlob = pdf.output('blob');
    
    // λ²„νŠΌμ„ λ‹€μ‹œ ν‘œμ‹œν•©λ‹ˆλ‹€ (PDF λ³€ν™˜μ΄ λλ‚œ ν›„)
    buttons.forEach(button => button.style.display = '');
    
    return pdfBlob;  // μƒμ„±λœ PDF Blob λ°˜ν™˜
  };

이λ ₯μ„œ μ €μž₯ - ν”„λ‘ νŠΈμ—”λ“œ

μ•„λž˜ μ½”λ“œλŠ” 이λ ₯μ„œλ₯Ό μ €μž₯ν•˜λŠ” ν”„λ‘ νŠΈμ—”λ“œ λ‘œμ§μž…λ‹ˆλ‹€.

μ‚¬μš©μžκ°€ 이λ ₯μ„œλ₯Ό PDF 파일둜 μ €μž₯ν•˜κ³  μ„œλ²„λ‘œ μ—…λ‘œλ“œν•˜λŠ” κ³Όμ •κ³Ό 그에 λŒ€ν•œ 응닡 처리λ₯Ό λ‹΄λ‹Ήν•©λ‹ˆλ‹€.




  const confirmAction = async () => {
    // λͺ¨λ‹¬μ—μ„œ '이λ ₯μ„œλ₯Ό μ €μž₯ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?' λ©”μ‹œμ§€κ°€ 보여지면 μ‹€ν–‰
    if (modalContent === 'μž‘μ„± λ‚΄μš©μ€ PDF 파일둜 μ €μž₯λ©λ‹ˆλ‹€
이λ ₯μ„œλ₯Ό μ €μž₯ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?') { try { setLoadingSave(true); // μ €μž₯ μ‹œμž‘ μ‹œ λ‘œλ”© λͺ¨λ‹¬ ν‘œμ‹œ // PDF 파일 생성 const pdfData = await generatePDF(); const formDataToSend = new FormData(); // μƒμ„±λœ PDF 데이터λ₯Ό FormData에 μΆ”κ°€ formDataToSend.append('file', new Blob([pdfData], { type: 'application/pdf' }), `${formData.resume_title}.pdf`); // 제λͺ©μ„ 파일 μ΄λ¦„μœΌλ‘œ μ„€μ • formDataToSend.append('title', formData.resume_title); // 이λ ₯μ„œ 제λͺ© formDataToSend.append('email', formData.email); // 이메일 formDataToSend.append('desired_company', formData.desired_company); // 희망 κΈ°μ—… // 이λ ₯μ„œ 파일 μ—…λ‘œλ“œ μš”μ²­ const uploadResponse = await axios.post('http://localhost:8080/api/resume/upload', formDataToSend, { headers: { 'Content-Type': 'multipart/form-data', // 파일 μ—…λ‘œλ“œ μ‹œ ν•„μš”ν•œ 헀더 }, }); const resumeId = uploadResponse.data.resumeId; // μ—…λ‘œλ“œλœ 이λ ₯μ„œ ID // 이λ ₯μ„œμ— λŒ€ν•œ μΆ”κ°€ 정보(μžκΈ°μ†Œκ°œ, 지원동기) μ €μž₯ await axios.post('http://localhost:8080/api/resume/proofread/save', { resumeId: resumeId, selfIntroduction: selfIntroduction, motivation: motivation }); // ν‚€μ›Œλ“œ μ—…λ°μ΄νŠΈ μš”μ²­ const keywordResponse = await axios.post('http://localhost:8080/api/resume/update-keywords', { resumeId: resumeId, selfIntroduction: selfIntroduction, motivation: motivation }); // λͺ¨λ‹¬ λ‹«κΈ° 및 확인 λͺ¨λ‹¬ μ—΄κΈ° setIsModalOpen(false); setIsConfirmationOpen(true); } catch (error) { console.error('μ—λŸ¬ λ°œμƒ:', error); // μ—λŸ¬ λ°œμƒ μ‹œ μ½˜μ†”μ— 둜그 좜λ ₯ } finally { setLoadingSave(false); // λ‘œλ”© λͺ¨λ‹¬ 숨기기 } } else { // 'μ €μž₯ν•˜μ§€ μ•Šκ³  λŒμ•„κ°€κΈ°' 선택 μ‹œ 이λ ₯μ„œ λͺ©λ‘ νŽ˜μ΄μ§€λ‘œ 이동 setIsModalOpen(false); router.push('/resume/resumeList'); } };
이λ ₯μ„œ μ €μž₯ - λ°±μ—”λ“œ

μ•„λž˜ μ½”λ“œλŠ” 이λ ₯μ„œ 관리 둜직 μ„œλΉ„μŠ€μž…λ‹ˆλ‹€.

μ‚¬μš©μžκ°€ μ—…λ‘œλ“œν•œ 이λ ₯μ„œλ₯Ό λ°μ΄ν„°λ² μ΄μŠ€μ— μ €μž₯ν•˜κ³ , 이λ ₯μ„œμ˜ ꡐ정 λ‚΄μš©κ³Ό ν‚€μ›Œλ“œλ₯Ό μ²˜λ¦¬ν•˜λŠ” μž‘μ—…μ„ λ‹΄λ‹Ήν•©λ‹ˆλ‹€.




  @Service
  public class ResumeService {

      @Autowired
      private ResumeRepository resumeRepository; // ResumeRepository μ£Όμž…

      @Autowired
      private ResumeProofreadRepository proofreadRepository; // ResumeProofreadRepository μ£Όμž…

      @Autowired
      private ExtractKeywordsService extractKeywordsService; // ExtractKeywordsService μ£Όμž…

      @Autowired
      private UserService userService; // UserService μ£Όμž…

      @Transactional
      public ResumeEntity saveResume(MultipartFile file, String title, String desiredCompany, User user) throws IOException {
          ResumeEntity resumeEntity = ResumeEntity.builder()
                  .resumePdf(file.getBytes()) // 이λ ₯μ„œ PDF 파일 μ €μž₯
                  .title(title) // 이λ ₯μ„œ 제λͺ© μ €μž₯
                  .desiredCompany(desiredCompany) // μž…μ‚¬ 희망 κΈ°μ—…λͺ… μ„€μ •
                  .user(user) // μ‚¬μš©μž 정보 μ„€μ •
                  .createdDate(LocalDateTime.now()) // 생성 λ‚ μ§œ μ„€μ •
                  .build();
          return resumeRepository.save(resumeEntity);  // μ €μž₯된 ResumeEntityλ₯Ό λ°˜ν™˜
      }

      @Transactional
      public void saveProofread(ResumeEntity resume, String selfIntroduction, String motivation) {
          ResumeProofreadEntity proofreadEntity = ResumeProofreadEntity.builder()
                  .resume(resume) // 이λ ₯μ„œμ™€ μ—°κ²°
                  .selfIntroduction(selfIntroduction) // μžκΈ°μ†Œκ°œ μ €μž₯
                  .motivation(motivation) // 지원동기 μ €μž₯
                  .build();
          proofreadRepository.save(proofreadEntity); // ꡐ정 λ‚΄μš© μ €μž₯
      }

      public List<ResumeEntity> findResumesByUser(User user) {
          return resumeRepository.findByUser(user); // μ‚¬μš©μžκ°€ μ—…λ‘œλ“œν•œ 이λ ₯μ„œ 리슀트 λ°˜ν™˜
      }

      public Optional<ResumeEntity> findResumeById(Long resumeId) {
          return resumeRepository.findById(resumeId); // 이λ ₯μ„œ ID둜 이λ ₯μ„œ 검색
      }

      @Transactional
      public void deleteResume(Long resumeId) {
          Optional<ResumeEntity> resumeOpt = resumeRepository.findById(resumeId); // 이λ ₯μ„œ μ°ΎκΈ°
          if (resumeOpt.isPresent()) {
              ResumeEntity resume = resumeOpt.get();
              proofreadRepository.deleteByResume(resume); // ν•΄λ‹Ή 이λ ₯μ„œμ˜ ꡐ정 λ‚΄μš© μ‚­μ œ
              resumeRepository.delete(resume); // 이λ ₯μ„œ μ‚­μ œ
          }
      }

      public Optional<ResumeProofreadEntity> getProofreadByResume(ResumeEntity resume) {
          return proofreadRepository.findByResume(resume); // 이λ ₯μ„œμ— λŒ€ν•œ ꡐ정 λ‚΄μš© 쑰회
      }

      public Optional<ResumeProofreadEntity> getProofreadByResumeId(Long resumeId) {
          return proofreadRepository.findByResume_ResumeId(resumeId); // 이λ ₯μ„œ ID둜 ꡐ정 λ‚΄μš© 쑰회
      }

      @Transactional
      public void updateKeywords(Long resumeId, String selfIntroduction, String motivation) throws IOException {
          Optional<ResumeEntity> resumeOpt = resumeRepository.findById(resumeId); // 이λ ₯μ„œ ID둜 이λ ₯μ„œ μ°ΎκΈ°
          if (resumeOpt.isPresent()) {
              ResumeEntity resume = resumeOpt.get();
              
              String[] keywordsSelfIntroduction = extractKeywordsService.extractKeywords(selfIntroduction); // μžκΈ°μ†Œκ°œμ—μ„œ ν‚€μ›Œλ“œ μΆ”μΆœ
              String[] keywordsMotivation = extractKeywordsService.extractKeywords(motivation); // μ§€μ›λ™κΈ°μ—μ„œ ν‚€μ›Œλ“œ μΆ”μΆœ
              
              resume.setKeywordsSelfIntroduction(String.join(", ", keywordsSelfIntroduction)); // ν‚€μ›Œλ“œ μžκΈ°μ†Œκ°œ μ €μž₯
              resume.setKeywordsMotivation(String.join(", ", keywordsMotivation)); // ν‚€μ›Œλ“œ 지원동기 μ €μž₯
              resumeRepository.save(resume); // 이λ ₯μ„œ μ €μž₯
          }
      }
  }
  

μ•„λž˜ μ½”λ“œλŠ” 이λ ₯μ„œ 관리 둜직 μ»¨νŠΈλ‘€λŸ¬μž…λ‹ˆλ‹€.

μ‚¬μš©μžκ°€ 이λ ₯μ„œλ₯Ό μ—…λ‘œλ“œ, 쑰회, λ‹€μš΄λ‘œλ“œ, μ‚­μ œν•˜κ³  κ΅μ •λœ λ‚΄μš©μ„ μ €μž₯ 및 μ‘°νšŒν•˜λŠ” κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€.

λ˜ν•œ, 이λ ₯μ„œμ— ν¬ν•¨λœ ν‚€μ›Œλ“œλ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” κΈ°λŠ₯도 ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.




  ResumeController.java

  @RestController
  @RequestMapping("/api/resume") // 이 URL 경둜둜 λ“€μ–΄μ˜€λŠ” μš”μ²­μ„ μ²˜λ¦¬ν•˜λŠ” 컨트둀러
  @CrossOrigin(origins = "http://localhost:3000") // CORS μ„€μ •: λ‹€λ₯Έ λ„λ©”μΈμ—μ„œ μš”μ²­μ„ ν—ˆμš©
  public class ResumeController {

      @Autowired
      private ResumeService resumeService; // 이λ ₯μ„œ 처리 μ„œλΉ„μŠ€

      @Autowired
      private UserService userService; // μ‚¬μš©μž μ„œλΉ„μŠ€
      
      @Autowired
      private ResumeProofreadRepository resumeProofreadRepository; // 이λ ₯μ„œ 첨삭 데이터 μ €μž₯μ†Œ

      @PostMapping("/upload") // 이λ ₯μ„œ μ—…λ‘œλ“œ API
      public ResponseEntity<?> uploadResume(@RequestParam("email") String email,
                                            @RequestParam("file") MultipartFile file,
                                            @RequestParam("title") String title,
                                            @RequestParam("desired_company") String desiredCompany) { // μ›ν•˜λŠ” κΈ°μ—…λͺ… μΆ”κ°€
          try {
              Optional<User> user = userService.findByEmail(email); // μ΄λ©”μΌλ‘œ μ‚¬μš©μž μ°ΎκΈ°
              if (user.isPresent()) { // μ‚¬μš©μžκ°€ μ‘΄μž¬ν•˜λ©΄
                  ResumeEntity savedResume = resumeService.saveResume(file, title, desiredCompany, user.get()); // 이λ ₯μ„œ μ €μž₯
                  return ResponseEntity.ok(Map.of("message", "이λ ₯μ„œκ°€ μ„±κ³΅μ μœΌλ‘œ μ—…λ‘œλ“œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.", "resumeId", savedResume.getResumeId())); // 성곡 응닡
              } else {
                  return ResponseEntity.status(HttpStatus.NOT_FOUND).body("μ‚¬μš©μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); // μ‚¬μš©μž μ—†μŒ
              }
          } catch (Exception e) { // μ˜ˆμ™Έ 처리
              return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("이λ ₯μ„œ μ—…λ‘œλ“œ 쀑 였λ₯˜ λ°œμƒ.");
          }
      }

      @GetMapping("/user-resumes") // μ‚¬μš©μžμ˜ λͺ¨λ“  이λ ₯μ„œ 쑰회 API
      public ResponseEntity<?> getUserResumes(@RequestParam("email") String email) {
          Optional<User> user = userService.findByEmail(email); // μ΄λ©”μΌλ‘œ μ‚¬μš©μž μ°ΎκΈ°
          if (user.isPresent()) { // μ‚¬μš©μžκ°€ μ‘΄μž¬ν•˜λ©΄
              List<ResumeEntity> resumes = resumeService.findResumesByUser(user.get()); // μ‚¬μš©μžμ— ν•΄λ‹Ήν•˜λŠ” 이λ ₯μ„œ λͺ©λ‘ 쑰회
              return ResponseEntity.ok(resumes); // 이λ ₯μ„œ λͺ©λ‘ λ°˜ν™˜
          } else {
              return ResponseEntity.status(HttpStatus.NOT_FOUND).body("μ‚¬μš©μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); // μ‚¬μš©μž μ—†μŒ
          }
      }

      @GetMapping("/download/{resumeId}") // 이λ ₯μ„œ λ‹€μš΄λ‘œλ“œ API
      public ResponseEntity<?> downloadResume(@PathVariable Long resumeId) throws UnsupportedEncodingException {
          Optional<ResumeEntity> resume = resumeService.findResumeById(resumeId); // 이λ ₯μ„œ ID둜 이λ ₯μ„œ μ°ΎκΈ°
          if (resume.isPresent()) { // 이λ ₯μ„œκ°€ μ‘΄μž¬ν•˜λ©΄
              ResumeEntity resumeEntity = resume.get(); // 이λ ₯μ„œ μ—”ν‹°ν‹° κ°€μ Έμ˜€κΈ°
              String resumeTitle = resumeEntity.getTitle().replaceAll("[^a-zA-Z0-9κ°€-힣]", "_") + ".pdf";  // 제λͺ©μ—μ„œ 특수문자λ₯Ό _둜 λŒ€μ²΄ν•˜κ³  ν™•μž₯자 μΆ”κ°€

              // UTF-8둜 μΈμ½”λ”©λœ 파일 이름을 μ§€μ›ν•˜κΈ° μœ„ν•΄ filename* μ‚¬μš©
              String encodedFilename = URLEncoder.encode(resumeTitle, StandardCharsets.UTF_8.toString()).replace("+", "%20"); // 파일λͺ… 인코딩
              
              ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok()
                  .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename) // 파일 λ‹€μš΄λ‘œλ“œ μ„€μ •
                  .header(HttpHeaders.CONTENT_TYPE, "application/pdf");  // MIME νƒ€μž… μ„€μ •

              return responseBuilder.body(resumeEntity.getResumePdf()); // PDF 파일 λ³Έλ¬Έ λ°˜ν™˜
          } else {
              return ResponseEntity.status(HttpStatus.NOT_FOUND).body("이λ ₯μ„œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); // 이λ ₯μ„œ μ—†μŒ
          }
      }

      @DeleteMapping("/delete/{resumeId}") // 이λ ₯μ„œ μ‚­μ œ API
      public ResponseEntity<?> deleteResume(@PathVariable Long resumeId) {
          try {
              resumeService.deleteResume(resumeId); // 이λ ₯μ„œ μ‚­μ œ
              return ResponseEntity.ok("이λ ₯μ„œκ°€ μ„±κ³΅μ μœΌλ‘œ μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€."); // 성곡 응닡
          } catch (Exception e) { // μ˜ˆμ™Έ 처리
              return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("이λ ₯μ„œ μ‚­μ œ 쀑 였λ₯˜ λ°œμƒ."); // 였λ₯˜ λ°œμƒ
          }
      }

      @GetMapping("/proofread/{resumeId}") // 이λ ₯μ„œ 첨삭 정보 쑰회 API
      public ResponseEntity<?> getProofread(@PathVariable Long resumeId) {
          Optional<ResumeProofreadEntity> proofread = resumeProofreadRepository.findByResume_ResumeId(resumeId); // 첨삭 정보 쑰회
          if (proofread.isPresent()) { // 첨삭 정보가 있으면
              Map<String, String> response = new HashMap<>();
              response.put("selfIntroduction", proofread.get().getSelfIntroduction()); // μžκΈ°μ†Œκ°œ 첨삭 λ‚΄μš©
              response.put("motivation", proofread.get().getMotivation()); // 동기 첨삭 λ‚΄μš©
              return ResponseEntity.ok(response); // 첨삭 λ‚΄μš© λ°˜ν™˜
          } else {
              return ResponseEntity.status(HttpStatus.NOT_FOUND).body("첨삭 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); // 첨삭 정보 μ—†μŒ
          }
      }

      @PostMapping("/proofread/save") // 첨삭 정보 μ €μž₯ API
      public ResponseEntity<?> saveProofread(@RequestBody Map<String, Object> requestData) {
          Long resumeId = Long.parseLong(requestData.get("resumeId").toString()); // 이λ ₯μ„œ ID
          String selfIntroduction = (String) requestData.get("selfIntroduction"); // μžκΈ°μ†Œκ°œ 첨삭 λ‚΄μš©
          String motivation = (String) requestData.get("motivation"); // 동기 첨삭 λ‚΄μš©

          Optional<ResumeEntity> resume = resumeService.findResumeById(resumeId); // 이λ ₯μ„œ 쑰회
          if (resume.isPresent()) { // 이λ ₯μ„œκ°€ μ‘΄μž¬ν•˜λ©΄
              resumeService.saveProofread(resume.get(), selfIntroduction, motivation); // 첨삭 λ‚΄μš© μ €μž₯
              return ResponseEntity.ok("AI 첨삭 정보가 μ„±κ³΅μ μœΌλ‘œ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€."); // 성곡 응닡
          } else {
              return ResponseEntity.status(HttpStatus.NOT_FOUND).body("이λ ₯μ„œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); // 이λ ₯μ„œ μ—†μŒ
          }
      }
      
      @PostMapping("/update-keywords") // ν‚€μ›Œλ“œ μ—…λ°μ΄νŠΈ API
      public ResponseEntity<?> updateKeywords(@RequestBody Map<String, Object> requestData) {
          Long resumeId = Long.parseLong(requestData.get("resumeId").toString()); // 이λ ₯μ„œ ID
          String selfIntroduction = (String) requestData.get("selfIntroduction"); // μžκΈ°μ†Œκ°œ
          String motivation = (String) requestData.get("motivation"); // 동기

          try {
              resumeService.updateKeywords(resumeId, selfIntroduction, motivation); // ν‚€μ›Œλ“œ μ—…λ°μ΄νŠΈ
              return ResponseEntity.ok("ν‚€μ›Œλ“œκ°€ μ„±κ³΅μ μœΌλ‘œ μ—…λ°μ΄νŠΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€."); // 성곡 응닡
          } catch (IOException e) { // μ˜ˆμ™Έ 처리
              return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("ν‚€μ›Œλ“œ μ—…λ°μ΄νŠΈ 쀑 였λ₯˜ λ°œμƒ."); // 였λ₯˜ λ°œμƒ
          }
      }
  }


λ§žμΆ€λ²• 검사 - ν•œμŠ€νŽ  라이브러리

μ•„λž˜ μ½”λ“œλŠ” express μ„œλ²„λ₯Ό μ‚¬μš©ν•˜μ—¬ ν•œκ΅­μ–΄ λ§žμΆ€λ²• 검사 κΈ°λŠ₯을 μ œκ³΅ν•˜λŠ” APIμž…λ‹ˆλ‹€.

CORS 섀정을 톡해 λ‹€λ₯Έ λ„λ©”μΈμ—μ„œμ˜ μš”μ²­μ„ ν—ˆμš©ν•˜κ³ , POST μš”μ²­μœΌλ‘œ 받은 λ¬Έμž₯을 hanspell λͺ¨λ“ˆμ„ μ‚¬μš©ν•΄ λ§žμΆ€λ²•μ„ κ²€μ‚¬ν•©λ‹ˆλ‹€.

μ„œλ²„λŠ” 포트 3001μ—μ„œ μ‹€ν–‰λ˜λ©°, ν΄λΌμ΄μ–ΈνŠΈκ°€ λ§žμΆ€λ²• 검사λ₯Ό μš”μ²­ν•˜λ©΄ κ²°κ³Όλ₯Ό JSON ν˜•μ‹μœΌλ‘œ λ°˜ν™˜ν•©λ‹ˆλ‹€.

였λ₯˜κ°€ λ°œμƒν•  경우 500 μƒνƒœ μ½”λ“œμ™€ ν•¨κ»˜ 였λ₯˜ λ©”μ‹œμ§€κ°€ μ „μ†‘λ©λ‹ˆλ‹€.




  // hanspellsever.js

  // express λͺ¨λ“ˆ
  const express = require('express');
  // hanspell λͺ¨λ“ˆ
  const hanspell = require('hanspell');
  // body-parser λͺ¨λ“ˆ - μš”μ²­ 본문을 νŒŒμ‹±ν•˜λŠ” 데 μ‚¬μš©
  const bodyParser = require('body-parser');
  // cors λͺ¨λ“ˆ - CORS 섀정을 μœ„ν•΄ μ‚¬μš©
  const cors = require('cors');  

  // express μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 객체λ₯Ό 생성
  const app = express();

  // CORS μ„€μ •
  app.use(cors({
    origin: 'http://localhost:3000', // μš”μ²­μ„ ν—ˆμš©ν•  좜처 μ„€μ •
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // ν—ˆμš©ν•  HTTP λ©”μ„œλ“œ μ„€μ •
    allowedHeaders: ['Content-Type', 'Authorization'], // ν—ˆμš©ν•  μš”μ²­ 헀더 μ„€μ •
  }));

  // λͺ¨λ“  κ²½λ‘œμ— λŒ€ν•΄ OPTIONS λ©”μ„œλ“œλ₯Ό μ²˜λ¦¬ν•˜λ„λ‘ μ„€μ •
  app.options('*', cors());

  // μš”μ²­ 본문을 JSON ν˜•μ‹μœΌλ‘œ νŒŒμ‹±ν•˜λ„λ‘ μ„€μ •
  app.use(bodyParser.json());

  // λ§žμΆ€λ²• 검사 API μ—”λ“œν¬μΈνŠΈ
  app.post('/check-spelling', (req, res) => {
    const sentence = req.body.sentence; // ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ 받은 λ¬Έμž₯

    let isResponseSent = false; // 응닡이 이미 μ „μ†‘λ˜μ—ˆλŠ”μ§€ μΆ”μ ν•˜λŠ” λ³€μˆ˜

    // hanspell λͺ¨λ“ˆμ„ μ‚¬μš©ν•˜μ—¬ λ§žμΆ€λ²• 검사
    hanspell.spellCheckByDAUM(
      sentence, // ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ 받은 λ¬Έμž₯
      6000, // μš”μ²­ μ‹œκ°„ 초과 μ‹œκ°„ (λ°€λ¦¬μ΄ˆ)
      (result) => {
        if (!isResponseSent) {
          isResponseSent = true; // 응닡을 λ³΄λƒˆμŒμ„ ν‘œμ‹œ
          res.json(result); // λ§žμΆ€λ²• 검사 κ²°κ³Όλ₯Ό JSON ν˜•μ‹μœΌλ‘œ 응닡
        }
      },
      (err) => {
        if (!isResponseSent) {
          isResponseSent = true; // 응닡을 λ³΄λƒˆμŒμ„ ν‘œμ‹œ
          res.status(500).send('Spelling check error'); // 였λ₯˜ λ°œμƒ μ‹œ 500 μƒνƒœ μ½”λ“œμ™€ ν•¨κ»˜ 였λ₯˜ λ©”μ‹œμ§€ 응닡
        }
      }
    );
  });

  // μ„œλ²„κ°€ 싀행될 포트 μ„€μ •
  const PORT = 3001;
  // μ„œλ²„λ₯Ό μ§€μ •λœ ν¬νŠΈμ—μ„œ μ‹€ν–‰
  app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`); // μ„œλ²„ μ‹€ν–‰ 확인 λ©”μ‹œμ§€
  });

μ•„λž˜ μ½”λ“œλŠ” 우츑 μ‚¬μ΄λ“œλ°”μ—μ„œ λ§žμΆ€λ²• 검사 κ²°κ³Όλ₯Ό λ³΄μ—¬μ£ΌλŠ” κΈ°λŠ₯을 κ΅¬ν˜„ν•œ λΆ€λΆ„μž…λ‹ˆλ‹€.

λ§žμΆ€λ²• 검사 κ²°κ³ΌλŠ” 'proofreadResult' 배열을 톡해 ν‘œμ‹œλ©λ‹ˆλ‹€.

배열에 데이터가 있으면 각 ν•­λͺ©μ„ 리슀트 ν˜•μ‹μœΌλ‘œ λ³΄μ—¬μ€λ‹ˆλ‹€.

검사 κ²°κ³Όκ°€ 없을 경우 "λ§žμΆ€λ²• 검사 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€."λΌλŠ” λ©”μ‹œμ§€κ°€ 좜λ ₯λ©λ‹ˆλ‹€.




  κ²°κ³Όν™”λ©΄ (우츑 μ‚¬μ΄λ“œλ°”)

  {isProofreadSidebarOpen && (  // isProofreadSidebarOpen이 true일 κ²½μš°μ—λ§Œ μ‚¬μ΄λ“œλ°”κ°€ 열리도둝 쑰건 처리
    <div className={`$ {proofreadStyles.proofreadSidebar} $ {isProofreadSidebarOpen ? proofreadStyles.open : ''} $ {isSidebarCollapsed ? proofreadStyles.collapsed : ''}`}> 
      <div className={proofreadStyles.sidebarHeader}>  // μ‚¬μ΄λ“œλ°”μ˜ 헀더 λΆ€λΆ„
        <h3 style=>λ§žμΆ€λ²• 검사 κ²°κ³Ό</h3>  // "λ§žμΆ€λ²• 검사 κ²°κ³Ό" 제λͺ©
        <div className={proofreadStyles.sidebarIcons}>  // μ‚¬μ΄λ“œλ°”μ— μžˆλŠ” μ•„μ΄μ½˜ μ˜μ—­
          {isSidebarCollapsed ? (  // μ‚¬μ΄λ“œλ°”κ°€ μ ‘ν˜€ 있으면 μ•„λž˜ ν™”μ‚΄ν‘œ μ•„μ΄μ½˜μ„ ν‘œμ‹œ
            <KeyboardArrowDownIcon onClick={toggleSidebarHeight} style= /> 
          ) : (  // μ‚¬μ΄λ“œλ°”κ°€ 펼쳐져 있으면 μœ„ ν™”μ‚΄ν‘œ μ•„μ΄μ½˜μ„ ν‘œμ‹œ
            <KeyboardArrowUpIcon onClick={toggleSidebarHeight} style= /> 
          )} 
          <button className={proofreadStyles.closeButton} onClick={closeProofreadSidebar}>  // μ‚¬μ΄λ“œλ°” λ‹«κΈ° λ²„νŠΌ
            <CloseIcon style= />  // λ‹«κΈ° μ•„μ΄μ½˜
          </button> 
        </div> 
      </div> 
      <div className={proofreadStyles.sidebarContent}>  // μ‚¬μ΄λ“œλ°”μ˜ λ‚΄μš© λΆ€λΆ„
        {proofreadResult.length > 0 ? (  // 검사 κ²°κ³Όκ°€ μžˆμ„ 경우
          <ul> 
            {proofreadResult.map((item, index) => (  // λ§žμΆ€λ²• 검사 κ²°κ³Ό 리슀트λ₯Ό μˆœνšŒν•˜μ—¬ 좜λ ₯
              <li key={index} className={proofreadStyles.resultItem}>  // 각 ν•­λͺ©μ— λŒ€ν•œ 리슀트 μ•„μ΄ν…œ
                <p><strong>잘λͺ»λœ ν‘œν˜„ :</strong> {item.token}</p>  // 잘λͺ»λœ ν‘œν˜„μ„ ν‘œμ‹œ
                <p><strong>μˆ˜μ • μ œμ•ˆ :</strong> {item.suggestions.join(', ')}</p>  // μˆ˜μ • μ œμ•ˆλ“€μ„ μ‰Όν‘œλ‘œ κ΅¬λΆ„ν•˜μ—¬ ν‘œμ‹œ
                <p><strong>μˆ˜μ • 이유 :</strong> {item.info}</p>  // μˆ˜μ • 이유λ₯Ό ν‘œμ‹œ
              </li> 
            ))} 
          </ul> 
        ) : (  // 검사 κ²°κ³Όκ°€ 없을 경우
          <p>λ§žμΆ€λ²• 검사 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.</p>  // κ²°κ³Όκ°€ μ—†μŒμ„ ν‘œμ‹œ
        )} 
      </div> 
    </div> 
  )}


AI첨삭 - gpt 3.5 turbo API 호좜 및 ν”„λ‘¬ν”„νŠΈ μž‘μ„±

μ•„λž˜ μ½”λ“œλŠ” ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ μ „μ†‘ν•œ ν…μŠ€νŠΈλ₯Ό λ°›μ•„ GPT에 ν…μŠ€νŠΈ 첨삭을 μš”μ²­ν•˜κ³ , κ²°κ³Όλ₯Ό λ°˜ν™˜ν•˜λŠ” RESTful API μ»¨νŠΈλ‘€λŸ¬μž…λ‹ˆλ‹€.

" / api / chatgpt - self " 둜 POST μš”μ²­μ΄ λ“€μ–΄μ˜€λ©΄, ν•΄λ‹Ή ν…μŠ€νŠΈλ₯Ό " ProofreadSelfService " 둜 μ „λ‹¬ν•˜μ—¬ 검사 κ²°κ³Όλ₯Ό λ°›μ•„μ˜΅λ‹ˆλ‹€.




  @RestController  // RESTful μ›Ή μ„œλΉ„μŠ€ 컨트둀러
  public class ProofreadSelfController {

      private final ProofreadSelfService proofreadService;  // ProofreadSelfService 객체λ₯Ό μ£Όμž…λ°›μ•„ μ‚¬μš©ν•  λ³€μˆ˜

      // μƒμ„±μž μ£Όμž…μ„ 톡해 ProofreadSelfServiceλ₯Ό λ°›μ•„μ˜΄
      public ProofreadSelfController(ProofreadSelfService proofreadService) {
          this.proofreadService = proofreadService;  // proofreadServiceλ₯Ό ν•΄λ‹Ή 클래슀의 멀버 λ³€μˆ˜μ— ν• λ‹Ή
      }

      // ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ /api/chatgpt-self둜 POST μš”μ²­μ„ 보내면 ν˜ΈμΆœλ˜λŠ” λ©”μ„œλ“œ
      @PostMapping("/api/chatgpt-self")
      public String getChatGPTResponse(@RequestBody Map<String, String> requestData) {  // μš”μ²­ λ°μ΄ν„°λŠ” Map ν˜•νƒœλ‘œ λ°›μ•„μ˜΄
          try {
              String text = requestData.get("text");  // μš”μ²­μ—μ„œ 'text' 값을 μΆ”μΆœ
              return proofreadService.getChatGPTResponse(text);  // proofreadService의 λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ ChatGPT 응닡을 λ°›μŒ
          } catch (IOException e) {  // IO μ˜ˆμ™Έ λ°œμƒ μ‹œ 처리
              e.printStackTrace();  // μ˜ˆμ™Έ λ©”μ‹œμ§€λ₯Ό μ½˜μ†”μ— 좜λ ₯
              return "Error occurred while processing your request: " + e.getMessage();  // μ—λŸ¬ λ©”μ‹œμ§€ λ°˜ν™˜
          } catch (Exception e) {  // κ·Έ μ™Έ λͺ¨λ“  μ˜ˆμ™Έ 처리
              e.printStackTrace();  // μ˜ˆμ™Έ λ©”μ‹œμ§€λ₯Ό μ½˜μ†”μ— 좜λ ₯
              return "An unexpected error occurred: " + e.getMessage();  // 일반적인 μ—λŸ¬ λ©”μ‹œμ§€ λ°˜ν™˜
          }
      }
  }

μ•„λž˜ μ½”λ“œλŠ” OpenAI GPT-3.5 λͺ¨λΈμ„ μ‚¬μš©ν•˜μ—¬ μ‚¬μš©μžκ°€ μž‘μ„±ν•œ μžκΈ°μ†Œκ°œμ„œλ₯Ό μ²¨μ‚­ν•˜λŠ” μ„œλΉ„μŠ€μž…λ‹ˆλ‹€.

API 킀와 URL을 μ„€μ •ν•˜κ³ , OkHttpλ₯Ό μ‚¬μš©ν•΄ API ν˜ΈμΆœμ„ 톡해 μžκΈ°μ†Œκ°œμ„œλ₯Ό λΆ„μ„ν•©λ‹ˆλ‹€.

비격식적인 문체λ₯Ό 격식 μžˆλŠ” 문체둜 μˆ˜μ •ν•˜κ³ , λͺ…ν™•μ„±κ³Ό 간결성을 λ†’μ΄λŠ” μˆ˜μ • μž‘μ—…μ„ μ§„ν–‰ν•©λ‹ˆλ‹€.

μˆ˜μ •λœ ν…μŠ€νŠΈμ™€ κ·Έ 이유λ₯Ό μ‚¬μš©μžμ—κ²Œ λ³΄μ—¬μ£ΌλŠ” λ°©μ‹μœΌλ‘œ λ™μž‘ν•©λ‹ˆλ‹€.




  @Service
  public class ProofreadSelfService {
      // API ν‚€λ₯Ό ν”„λ‘œνΌν‹° νŒŒμΌμ—μ„œ κ°€μ Έμ˜΅λ‹ˆλ‹€
      @Value("${proofread.api-key}")
      private String apiKey;

      // API URL μ„€μ •
      private static final String API_URL = "https://api.openai.com/v1/chat/completions";

      // ObjectMapper 객체 생성 (JSON 처리용)
      private final ObjectMapper objectMapper = new ObjectMapper();

      // ChatGPT API둜 μš”μ²­μ„ 보내고 응닡을 λ°›λŠ” λ©”μ†Œλ“œ
      public String getChatGPTResponse(String text) throws IOException {
          // OkHttpClientλ₯Ό μ„€μ •ν•˜μ—¬ API ν˜ΈμΆœμ„ μœ„ν•œ ν΄λΌμ΄μ–ΈνŠΈλ₯Ό 생성
          OkHttpClient client = new OkHttpClient.Builder()
              .connectTimeout(120, TimeUnit.SECONDS)
              .writeTimeout(120, TimeUnit.SECONDS)
              .readTimeout(120, TimeUnit.SECONDS)
              .build();

          // ν”„λ‘¬ν”„νŠΈ 생성: μ‚¬μš©μžκ°€ μž‘μ„±ν•œ μžκΈ°μ†Œκ°œλ₯Ό λΆ„μ„ν•˜κ³  첨삭을 μš”μ²­ν•˜λŠ” ν…μŠ€νŠΈ
          StringBuilder promptBuilder = new StringBuilder();
          promptBuilder.append("μš°λ¦¬λŠ” μ›ΉνŽ˜μ΄μ§€μ˜ μ΄μš©μžκ°€ 이λ ₯μ„œμ˜ 'μžκΈ°μ†Œκ°œ' νŒŒνŠΈμ— μž‘μ„±ν•œ ν…μŠ€νŠΈλ₯Ό 기반으둜 μžκΈ°μ†Œκ°œ 첨삭을 ν• κ±°μ•Ό.");
          promptBuilder.append("μ΄μš©μžλŠ” 아직 νšŒμ‚¬μ— μž…μ‚¬ν•˜μ§€ μ•Šμ€ μƒνƒœκ³ , νšŒμ‚¬ μž…μ‚¬λ₯Ό μœ„ν•œ 이λ ₯μ„œμ˜ μžκΈ°μ†Œκ°œλž€μ— μžκΈ°μ†Œκ°œλ₯Ό μ κ³ μžˆλŠ” 상황이야.");
          promptBuilder.append("νšŒμ‚¬ μž…μ‚¬λ₯Ό μœ„ν•œ 곡식적인 μžκΈ°μ†Œκ°œμ„œ μž‘μ„±μ΄λ‹ˆκΉŒ μ΄μš©μžλŠ” 곡식적이고 κ²©μ‹μžˆλŠ” 문체둜 ν…μŠ€νŠΈλ₯Ό μž‘μ„±ν•˜κ² μ§€.");
          promptBuilder.append("μš°λ¦¬λŠ” 이 ν…μŠ€νŠΈλ₯Ό μ‚¬μš©μžμ—κ²Œ λ°›μ•„μ„œ λΆ„μ„ν•œ λ’€ AI첨삭을 ν•΄μ£ΌλŠ” 역할을 ν•˜λŠ”κ±°μ•Ό.");
          promptBuilder.append("μ΄μš©μžμ—κ²Œ 첨삭 κ²°κ³Όλ₯Ό 보여쀄 λ•ŒλŠ” λ°˜λ“œμ‹œ μ‘΄λŒ“λ§μ„ μ‚¬μš©ν•˜κ³  μΌμ •ν•œ μ–΄νˆ¬λ₯Ό μœ μ§€ν•΄μ•Ό ν•΄.");
          promptBuilder.append("ν…μŠ€νŠΈλ₯Ό 읽고 μžκΈ°μ†Œκ°œ 첨삭을 ν•΄μ£ΌλŠ” 기쀀을 μ•Œλ €μ€„κ²Œ. 그에 맞게 λ„ˆκ°€ λ©”μ‹œμ§€λ₯Ό ν‘œμ‹œν•΄μ£Όλ©΄ 돼.");
          promptBuilder.append("첫번째 기쀀은 μ΄μš©μžκ°€ μž‘μ„±ν•œ ν…μŠ€νŠΈκ°€ 곡식적인 이λ ₯μ„œ μž‘μ„±μ— λ§žμ§€ μ•ŠλŠ” 문체인 κ²½μš°μ•Ό.");
          promptBuilder.append("비격식적인 ν‘œν˜„μ΄λ‚˜ ꡬ어체λ₯Ό μ‚¬μš©ν•˜λŠ” 경우 μˆ˜μ •μ„ ν•΄μ€˜. μ‚¬μš©μžκ°€ μžμ‹ μ„ ν‘œν˜„ν•  λ•ŒλŠ” 'λ‚˜', 'λ‚΄κ°€'라고 μ μ—ˆμ„κ²½μš° 'μ €', 'μ œκ°€'둜 μˆ˜μ •ν•΄μ€˜.");
          promptBuilder.append("λ‘λ²ˆμ§Έ 기쀀은 λͺ…λ£Œμ„±κ³Ό 간결성이야. λΉ„μŠ·ν•œ λ‹¨μ–΄λ‚˜ λ¬Έμž₯이 계속 μ‚¬μš©λ˜κ±°λ‚˜ λ¬Έμž₯이 μ™„μ „ν•˜κ²Œ λλ‚˜μ§€ μ•Šμ€ λ¬Έμž₯이 μžˆλŠ”μ§€ νŒŒμ•…ν•΄μ£Όκ³  μžˆλ‹€λ©΄ λ¬Έμž₯을 κ°„κ²°ν•˜κ³  λͺ…ν™•ν•˜κ²Œ λλ‚˜κ²Œ μˆ˜μ •ν•΄μ£Όλ©΄ 돼.");
          promptBuilder.append("λ¬Έμž₯이 μ™„μ „ν•˜κ²Œ λλ‚˜μ§€ μ•Šμ€ λ¬Έμž₯의 μ˜ˆμ‹œλ‘œλŠ” '은,λŠ”,이,κ°€' λ“±μœΌλ‘œ λ¬Έμž₯이 λΆˆμ™„μ „ν•˜κ²Œ λλ‚˜λŠ” κ²½μš°κ°€ μžˆκ² μ§€. λ˜ν•œ λͺ…μ‚¬λ‘œ λ¬Έμž₯이 λλ‚˜λ²„λ¦¬λŠ” κ²½μš°μ—λ„ μ™„μ „ν•œ λ¬Έμž₯으둜 μˆ˜μ •ν•΄μ€˜.");
          promptBuilder.append("μœ„μ˜ 기쀀듀에 따라 μ‚¬μš©μžμ˜ ν…μŠ€νŠΈλ₯Ό μˆ˜μ •ν•˜μ—¬ 첨삭 κ²°κ³Ό λ©”μ‹œμ§€λ₯Ό λ„μšΈ λ•Œ, 'β–Ά 첨삭 κ²°κ³ΌλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€."둜 제λͺ©μ„ 보여주고 밑에 μˆ˜μ • κ²°κ³Ό λ©”μ‹œμ§€λ₯Ό λ„μ›Œμ€˜.");
          promptBuilder.append("λ°˜λ“œμ‹œ μˆ˜μ •μ΄ μ™„λ£Œλœ μ‚¬μš©μžμ˜ ν…μŠ€νŠΈ 전체 λ¬Έμž₯을 ν•œ λ²ˆμ— λ³΄λ‚΄μ€˜.");
          promptBuilder.append("λ˜ν•œ μ‚¬μš©μžμ˜ ν…μŠ€νŠΈλ₯Ό μˆ˜μ •ν•  λ•ŒλŠ” μš”μ•½μ„ ν•˜κ±°λ‚˜ κΈ€μ˜ 흐름을 λ°”κΎΈλ©΄ μ•ˆλΌ.");
          promptBuilder.append("μ›λž˜μ˜ λ¬Έμž₯ ꡬ쑰λ₯Ό μœ μ§€ν•˜λ˜, μœ„μ˜ 기쀀에 λ§žμ§€ μ•ŠλŠ” λΆ€λΆ„λ§Œ μˆ˜μ •ν•˜λŠ” μ‹μœΌλ‘œ ν•΄μ•Ό ν•΄.");

          promptBuilder.append("μˆ˜μ •λœ ν…μŠ€νŠΈ 전체 λ¬Έμž₯을 λ³΄λƒˆλ‹€λ©΄, λ‹€μ‹œ 두 쀄 λ„μš°κ³  'β–Ά μˆ˜μ • 뢀뢄은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€."둜 제λͺ©μ„ 보여주고 밑에 μˆ˜μ • 이유 λ©”μ„Έμ§€λ₯Ό λ³΄μ—¬μ€˜.");
          promptBuilder.append("μˆ˜μ • 이유λ₯Ό λ³΄μ—¬μ€„λ•ŒλŠ” - ν•˜μ΄ν‘ΌμœΌλ‘œ 틀을 μ‹œμž‘ν•˜κ³  'μˆ˜μ •μ΄μœ ' : 'μˆ˜μ • μ „ λ¬Έμž₯' β†’ 'μˆ˜μ • ν›„ λ¬Έμž₯' 이런 ν˜•μ‹μ΄ ν•˜λ‚˜μ˜ 틀이라고 μƒκ°ν•˜λ©΄λΌ. ν•˜λ‚˜μ˜ ν‹€μ—λŠ” ν•˜λ‚˜μ˜ λ°˜λ“œμ‹œ ν•˜λ‚˜μ˜ ν•˜μ΄ν‘Όλ§Œ λ“€μ–΄κ°€μ•Όν•΄. λ”°λΌμ„œ λ°˜λ“œμ‹œ 'μˆ˜μ •μ΄μœ ' μ•žμ—λ§Œ ν•˜μ΄ν‘Όμ΄ λΆ™μ–΄μ•Όκ² μ§€. ");
          
          promptBuilder.append("μœ„μ˜ ν‹€μ—μ„œ 'μˆ˜μ • 이유' μ—λŠ” λ„ˆκ°€ μˆ˜μ •μ„ ν•œ μ΄μœ κ°€ λ“€μ–΄κ°€μ•Όν•˜κ³  'μˆ˜μ • μ „ λ¬Έμž₯'μ—λŠ” μˆ˜μ •μ„ 거치기 μ „ μ‚¬μš©μžμ˜ ν…μŠ€νŠΈ μ›λ³Έλ§Œ λ“€μ–΄κ°€μ•Όν•΄. 'μˆ˜μ • ν›„ λ¬Έμž₯'은 λ„ˆκ°€ μˆ˜μ •μ„ μ™„λ£Œν•œ λ¬Έμž₯만 λ“€μ–΄κ°€μ•Όν•΄.");
          promptBuilder.append("μˆ˜μ •μ΄μœ  틀인 'μˆ˜μ • μ „ λ¬Έμž₯' κ³Ό 'μˆ˜μ • ν›„ λ¬Έμž₯' 이 ν…μŠ€νŠΈλŠ” ν¬ν•¨μ‹œν‚€μ§€ 마. 이건 λ‚΄κ°€ λ„ˆμ—κ²Œ μ•Œλ €μ£ΌλŠ” 틀일뿐이야. μ € ν‹€μ•ˆμ— λ‚΄κ°€ μš”μ²­ν•œ λ¬Έμž₯만 μ‚¬μš©μžμ—κ²Œ 보여주면돼. ");
          promptBuilder.append("λ„ˆκ°€ μ΄ν•΄ν•˜κΈ° μ‰½κ²Œ μˆ˜μ •μ΄μœ  μ˜ˆμ‹œλ₯Ό λ³΄μ—¬μ£Όμžλ©΄ λ‹€μŒκ³Όκ°™μ•„. ");
          promptBuilder.append(" - μˆ˜μ •μ΄μœ  : 문체가 비격식적인 ν‘œν˜„μ„ ν¬ν•¨ν•˜κ³  μžˆμ–΄ 격식 μžˆλŠ” 문체둜 μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€.\r\n");
          promptBuilder.append("'λŒ€ν•™κ΅μ—μ„œ μ—¬λŸ¬ κ°€μ§€ ν”„λ‘œμ νŠΈλ₯Ό μ§„ν–‰ν–ˆμŠ΅λ‹ˆλ”." β†’ "'λŒ€ν•™κ΅μ—μ„œ μ—¬λŸ¬ κ°€μ§€ ν”„λ‘œμ νŠΈλ₯Ό μ§„ν–‰ν–ˆμŠ΅λ‹ˆλ‹€."");
          promptBuilder.append("μœ„μ˜ μ˜ˆμ‹œλ₯Ό μ°Έκ³ ν•΄μ„œ 같은 ν‹€κ³Ό ν˜•μ‹μœΌλ‘œ μˆ˜μ •μ΄μœ λ₯Ό 보여주면돼.");
          promptBuilder.append("<ν•„μˆ˜>");
          promptBuilder.append("μœ„μ—μ„œ μ–ΈκΈ‰ν•œ λ‚΄μš©λ“€μ„ λͺ¨λ‘ λ°˜λ“œμ‹œ μ§€μΌœμ•Ό ν•΄.");
          promptBuilder.append("그리고 λ„ˆλŠ” 첨삭 κ²°κ³Ό μ™Έμ—λŠ” 아무것도 ν‘œμ‹œν•˜λ©΄ μ•ˆ 돼.");
          promptBuilder.append("λ„ˆκ°€ μˆ˜μ •ν•œ 뢀뢄듀은 ν•˜λ‚˜λ„ λΉ λœ¨λ¦¬μ§€ μ•Šκ³  λ°˜λ“œμ‹œ λͺ¨λ‘ μˆ˜μ •μ΄μœ  λ©”μ‹œμ§€λ‘œ μ‚¬μš©μžμ—κ²Œ λ³΄μ—¬μ€˜μ•Όν•΄.");
          promptBuilder.append("λ‚΄κ°€ λ„ˆμ—κ²Œ μ£ΌλŠ” μ§€μ‹œ μ‚¬ν•­μ΄λ‚˜, λ„ˆκ°€ λ‚˜ν•œν…Œ λŒ€λ‹΅ν•˜λŠ” λ‚΄μš©μ€ μ ˆλŒ€λ‘œ 첨삭 결과에 ν¬ν•¨λ˜λ©΄ μ•ˆ 돼.");
          promptBuilder.append("κ²°κ³Όμ—λŠ” 였직 첨삭 λ©”μ‹œμ§€μ™€ κ΄€λ ¨λœ λ‚΄μš©λ§Œ ν¬ν•¨μ‹œν‚€κ³ , κ·Έ μ™Έμ˜ λΆˆν•„μš”ν•œ ν…μŠ€νŠΈλ‚˜ λ‚΄μš©μ€ μ ˆλŒ€λ‘œ ν¬ν•¨μ‹œν‚€μ§€ 마.");
          promptBuilder.append("μ‚¬μš©μžμ—κ²ŒλŠ” 였직 첨삭 결과와 μˆ˜μ • 이유만 λ³΄μ—¬μ€˜μ•Ό ν•΄.");
          promptBuilder.append("자 그럼 μ•„λž˜ ν…μŠ€νŠΈλ₯Ό 읽고 μœ„μ˜ μ§€μ‹œμ‚¬ν•­μ— 맞게 첨삭 κ²°κ³Όλ₯Ό 좜λ ₯ν•΄μ€˜.");
          promptBuilder.append(text);
          promptBuilder.append("ν…μŠ€νŠΈλ₯Ό λΆ„μ„ν• λ•ŒλŠ” λ°˜λ“œμ‹œ 원본 κ·ΈλŒ€λ‘œ 뢄석을 ν•œ λ’€ 첨삭을 μ§„ν–‰ν•΄μ•Όν•΄.");

          String prompt = promptBuilder.toString();

          // JSON μš”μ²­ λ³Έλ¬Έ 생성
          Map<String, Object> jsonBody = new HashMap<>();
          jsonBody.put("model", "gpt-3.5-turbo");

          Map<String, String> message = new HashMap<>();
          message.put("role", "user");
          message.put("content", prompt);

          jsonBody.put("messages", new Object[] { message });

          MediaType mediaType = MediaType.parse("application/json");
          RequestBody body = RequestBody.create(objectMapper.writeValueAsString(jsonBody), mediaType);

          // API 호좜
          Request request = new Request.Builder()
              .url(API_URL)
              .addHeader("Authorization", "Bearer " + apiKey)
              .post(body)
              .build();

          Response response = client.newCall(request).execute();
          if (response.isSuccessful()) {
              // 응닡 받은 JSONμ—μ„œ κ²°κ³Ό ν…μŠ€νŠΈ μΆ”μΆœ
              String responseBody = response.body().string();
              JsonNode root = objectMapper.readTree(responseBody);
              JsonNode choices = root.path("choices");
              JsonNode messageNode = choices.get(0).path("message");
              return messageNode.path("content").asText();
          } else {
              throw new IOException("Failed to get response from API");
          }
      }
  }



4. DB 섀계

γ…€λ°μ΄ν„°λ² μ΄μŠ€λŠ” Oracle DB λ₯Ό μ‚¬μš©ν•˜μ—¬ μ„€κ³„ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

γ…€μž‘μ„±ν•œ 이λ ₯μ„œ (Resume Entity + Resume Proofread Entity) λŠ” μ‚¬μš©μž 정보 (User Entity) 에 μ €μž₯λ©λ‹ˆλ‹€.

γ…€μ €μž₯된 이λ ₯μ„œ 정보λ₯Ό λ°”νƒ•μœΌλ‘œ AI λ©΄μ ‘ (Videos Entity) 이 μ΄λ£¨μ–΄μ§‘λ‹ˆλ‹€.

Resume Entity

ResumeEntity ν΄λž˜μŠ€λŠ” 이λ ₯μ„œλ₯Ό μ €μž₯ν•˜κΈ° μœ„ν•œ JPA μ—”ν‹°ν‹°μž…λ‹ˆλ‹€.

User와 λ‹€λŒ€μΌ(@ManyToOne) 관계λ₯Ό λ§Ίκ³  있으며, LAZY λ‘œλ”©μ„ μ‚¬μš©ν•΄ ν•„μš” μ‹œ λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ User 정보λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€.




  @Data // Lombok μ–΄λ…Έν…Œμ΄μ…˜ - Getter, Setter, equals, hashCode, toString λ©”μ„œλ“œ μžλ™ 생성
  @Entity // JPA μ–΄λ…Έν…Œμ΄μ…˜ - λ°μ΄ν„°λ² μ΄μŠ€ ν…Œμ΄λΈ”κ³Ό λ§€ν•‘
  @Builder // Lombok μ–΄λ…Έν…Œμ΄μ…˜ - λΉŒλ” νŒ¨ν„΄ μ‚¬μš©ν•΄ 객체 생성
  @NoArgsConstructor // Lombok μ–΄λ…Έν…Œμ΄μ…˜ - νŒŒλΌλ―Έν„°κ°€ μ—†λŠ” κΈ°λ³Έ μƒμ„±μž 생성
  @AllArgsConstructor // Lombok μ–΄λ…Έν…Œμ΄μ…˜ - λͺ¨λ“  ν•„λ“œλ₯Ό νŒŒλΌλ―Έν„°λ‘œ λ°›λŠ” μƒμ„±μž 생성
  @Table(name = "resume") // JPA μ–΄λ…Έν…Œμ΄μ…˜ - λ§€ν•‘λ˜λŠ” λ°μ΄ν„°λ² μ΄μŠ€ ν…Œμ΄λΈ” 이름 μ§€μ •

  public class ResumeEntity {
    @ManyToOne(fetch = FetchType.LAZY) // ResumeEntity - Userκ°€ λ‹€λŒ€μΌ 관계 - μ§€μ—° λ‘œλ”©(FetchType.LAZY) μ„€μ •μœΌλ‘œ ν•„μš”ν•  λ•Œλ§Œ User 데이터 λ‘œλ“œ
    @JoinColumn(name = "user_id", nullable = false) // user_id 컬럼이 User ν…Œμ΄λΈ”μ˜ κΈ°λ³Έ 킀와 쑰인
    private User user; // 이λ ₯μ„œλ₯Ό μž‘μ„±ν•œ μ‚¬μš©μž 정보
    
      @Id // κΈ°λ³Έ ν‚€
      @SequenceGenerator(name = "resume_seq", sequenceName = "resume_seq", allocationSize = 1, initialValue = 1) // μ‹œν€€μŠ€ 톡해 κΈ°λ³Έ ν‚€ 생성. allocationSizeλŠ” 증가 κ°’, initialValueλŠ” 초기 κ°’
      @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "resume_seq") // κΈ°λ³Έ ν‚€ 값을 μ‹œν€€μŠ€ 톡해 μžλ™ 생성
      @Column(name = "resume_id") // λ°μ΄ν„°λ² μ΄μŠ€μ˜ resume_id μ»¬λŸΌμ— λ§€ν•‘
      private Long resumeId; // 이λ ₯μ„œ 고유 ID

      @Lob // λŒ€μš©λŸ‰ λ°”μ΄λ„ˆλ¦¬ 데이터 μ €μž₯
      @Column(name = "resume_pdf", nullable = false)
      private byte[] resumePdf; // 이λ ₯μ„œ PDF 파일 데이터
      
      @Column(name = "created_date", nullable = false)
      private LocalDateTime createdDate; // 이λ ₯μ„œ 생성 λ‚ μ§œ 및 μ‹œκ°„
      
      @Column(name = "title", nullable = false)
      private String title; // 이λ ₯μ„œ 제λͺ©
      
      @Column(name = "keywords_self_introduction", length = 2000)
      private String keywordsSelfIntroduction; // μžκΈ°μ†Œκ°œ ν‚€μ›Œλ“œ
      

      @Column(name = "keywords_motivation", length = 2000)
      private String keywordsMotivation; // 지원 동기 ν‚€μ›Œλ“œ
      
      @Column(name = "desired_company")
      private String desiredCompany; // μ§€μ›μžκ°€ ν¬λ§ν•˜λŠ” νšŒμ‚¬λͺ…
  }

Resume Proofread Entity

ResumeProofreadEntity ν΄λž˜μŠ€λŠ” λ°μ΄ν„°λ² μ΄μŠ€ ν…Œμ΄λΈ” resume_proofread와 λ§€ν•‘λ˜λŠ” JPA μ—”ν‹°ν‹°μž…λ‹ˆλ‹€.

이λ ₯μ„œ 첨삭 λ‚΄μš©μ„ μ €μž₯ν•˜λ©° μžκΈ°μ†Œκ°œμ„œ 첨삭, 지원동기 첨삭 정보λ₯Ό ν¬ν•¨ν•©λ‹ˆλ‹€.




  @Data // Lombok μ–΄λ…Έν…Œμ΄μ…˜ - Getter, Setter, equals, hashCode, toString λ©”μ„œλ“œ μžλ™ 생성
  @Entity // JPA μ–΄λ…Έν…Œμ΄μ…˜ - λ°μ΄ν„°λ² μ΄μŠ€ ν…Œμ΄λΈ”κ³Ό λ§€ν•‘
  @Builder // Lombok μ–΄λ…Έν…Œμ΄μ…˜ - λΉŒλ” νŒ¨ν„΄ μ‚¬μš©ν•΄ 객체 생성
  @NoArgsConstructor // Lombok μ–΄λ…Έν…Œμ΄μ…˜ - νŒŒλΌλ―Έν„°κ°€ μ—†λŠ” κΈ°λ³Έ μƒμ„±μž 생성
  @AllArgsConstructor // Lombok μ–΄λ…Έν…Œμ΄μ…˜ - λͺ¨λ“  ν•„λ“œλ₯Ό νŒŒλΌλ―Έν„°λ‘œ λ°›λŠ” μƒμ„±μž 생성
  @Table(name = "resume_proofread") // JPA μ–΄λ…Έν…Œμ΄μ…˜ - λ§€ν•‘λ˜λŠ” λ°μ΄ν„°λ² μ΄μŠ€ ν…Œμ΄λΈ” 이름 μ§€μ •

  public class ResumeProofreadEntity {

      @Id
      @SequenceGenerator(
          name = "proofread_seq",
          sequenceName = "proofread_seq",
          allocationSize = 1,
          initialValue = 1
      )
      @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "proofread_seq")
      @Column(name = "proofread_id")
      private Long proofreadId; // κ²€ν†  고유 ID

      @ManyToOne(fetch = FetchType.LAZY) // JPA μ–΄λ…Έν…Œμ΄μ…˜: `ResumeEntity`와 λ‹€λŒ€μΌ 관계 μ„€μ •, μ§€μ—° λ‘œλ”©(FetchType.LAZY) μ‚¬μš©
      @JoinColumn(name = "resume_id", nullable = false) // λ°μ΄ν„°λ² μ΄μŠ€μ˜ `resume_id` 컬럼과 λ§€ν•‘. `ResumeEntity`의 κΈ°λ³Έ 킀와 쑰인
      private ResumeEntity resume; // κ²€ν†  λŒ€μƒ 이λ ₯μ„œ

      @Lob // JPA μ–΄λ…Έν…Œμ΄μ…˜: λŒ€μš©λŸ‰ 데이터(ν…μŠ€νŠΈ)λ₯Ό μ €μž₯ν•˜κΈ° μœ„ν•΄ μ‚¬μš©
      @Column(name = "self_introduction", nullable = false)
      private String selfIntroduction; // μžκΈ°μ†Œκ°œμ„œ κ²€ν†  λ‚΄μš©

      @Lob // λŒ€μš©λŸ‰ 데이터(ν…μŠ€νŠΈ)λ₯Ό μ €μž₯ν•˜κΈ° μœ„ν•œ μ–΄λ…Έν…Œμ΄μ…˜
      @Column(name = "motivation", nullable = false)
      private String motivation; // 지원 동기 κ²€ν†  λ‚΄μš©
  }

γ…€κ΄€κ³„λ„λŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

이 κΈ°μ‚¬λŠ” μ €μž‘κΆŒμžμ˜ CC BY 4.0 λΌμ΄μ„ΌμŠ€λ₯Ό λ”°λ¦…λ‹ˆλ‹€.