π ν νλ‘μ νΈ λ΄λΉ ννΈ - μ΄λ ₯μ λ±λ‘
ν νλ‘μ νΈμμ μ κ° λ΄λΉν ννΈμΈ 'μ΄λ ₯μ λ±λ‘' μ λν μμΈ μ€λͺ μ λλ€.
μ΄λ ₯μ λ±λ‘ νμ΄μ§ λ§λ€κΈ°
1. ννΈ μ€λͺ
γ €AI λΉλλ©΄ λ©΄μ μ°μ΅μ μν΄ focusjob μ¬μ΄νΈλ₯Ό λ°©λ¬Έν μ¬μ©μλ€μ νμμ μΌλ‘ μ΄λ ₯μλ₯Ό λ±λ‘ν΄μΌν©λλ€.
γ €μ΄μ λ λ±λ‘ν μ¬μ©μμ μ΄λ ₯μ μ 보λ₯Ό λ°νμΌλ‘ AI λ©΄μ μ§λ¬Έμ΄ λ§μΆ€ μμ±λκΈ° λλ¬Έμ λλ€.
γ €μ΄λ ₯μ κ΄λ ¨ νμ΄μ§λ μΈκ°μ ννΈλ‘ λλ©λλ€.
μ΄λ ₯μ λ±λ‘
μ΄λ ₯μ κ΄λ¦¬
λ§μΆ€λ² κ²μ¬ λ° AI첨μ
γ €γ €νμ΄μ§ λ΄μμ μ¬μ©λλ μ£Όμ κΈ°λ₯μ μλμ κ°μ΅λλ€.
| [ κΈ°λ₯ ] | [ μ€λͺ ] |
|---|---|
| UI | React (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; // μ§μ λκΈ° κ²ν λ΄μ©
}
γ €κ΄κ³λλ μλμ κ°μ΅λλ€.