Skip to content

Commit 81b18eb

Browse files
dbfgml2000dooongdaengtlswltjqsso0omluckhee
authored
1차 코드리뷰 (#54)
* chore: Update README.md * [feat] SpringDoc 설정 (#13) * chore: Update README.md (#2) * feat: SpringDoc 설정 * feat:addrefreshtoken * feat:entityã�생성 * Test:AuthTokenServiceTest * feat:회원가입시 역할 나누기 * refactor:idType * Feat : Comment, News 엔티티 작성. * Feat : likes필드 추가 * Feat : Video 작성 및 News 와 연관관계 매핑 * Feat : 비디오 픽스쳐 메소드 작성 * Test : News 도메인 테스트 추가 * Refactor : 생성자를 통해 초기화되도록 수정 * Feat : News의 유효성 검증 로직 추가 * Test : 테스트 추가 * Test : 생성 테스트가 코멘트 리스트 크기를 체크 * Fix : 생성자를 통해 코멘트 리스트를 초기화하도록 수정 * Feat : News와 Member가 연관관계를 맺도록 매핑 * Feat : Member 픽스쳐 메소드 작성 * Test : Member 연관관계 매핑에 따른 테스트케이스 추가 및 수정 * Feat : News 픽스쳐 메소드 작성 * Feat : Comment 유효성 검증 로직 추가 * Test : 테스트 추가 * Feat : Video 유효성 검증 로직 추가 * Chore : 패키지 위치 변경 * Test : 테스트 추가 * Feat : News 도메인 Repository, Service 작성 * Feat : Like 엔티티 작성 * Feat : 좋아요 집계를 정수가 아닌 Like 엔티티로 변경, 메서드 추가 * Feat : 메서드 추가 * Fix : News에서 추가되도록 수정 * Fix : News에서 추가되도록 수정 * Chore : 메모 * Test : 테스트 추가 * Fix : News엔티티를 통해 삭제하도록 수정 * Test : 테스트 추가 * Feat : file 도메인 Service, Repository 작성 * Feat : comment 도메인 Service, Repository 작성 * Feat : Like 도메인 Service, Repository 작성 * Feat : NewsService 작성 * Feat : News로부터 Comment접근 가능하기떄문에 굳이 작성 안해도 될 것 같다, 다만 컨트롤러는 분리하는것이 좋다는 강사님 의견 반영 * Feat : JpaRepository 상속받도록함 * chore: 멘토링 관련 엔티티 생성 * chore: 멘토링 관련 레파지토리 생성 * Feat : S3 사용전 모킹을 위한 MiniO * Feat : 파일 다운로드를 위한 설정 및 메서드 작성 * feat: 로드맵 도메인 엔티티 구현 (#21) * Feat : 메타데이터 저장할 메서드 createVideo, Dash파일 다운용 메서드 generateDashUrls()추가 * Feat : 메서드 추가 * Feat : news 생성 엔드포인트 작성 * Feat : 코멘트 컨트롤러 생성 * Feat : 메서드 오버로딩 * Feat : 예외처리 * Feat : HEAD요청을 통해 객체 존재하는지 미리 확인 * Test : 테스트 추가 * Feat : Like 서비스, 레포지토리 작성 * Feat : News단건, 다건조회 엔드포인트 작성 * Test : Like 테스트 작성 * Feat : 좋아요기능 작성 * Fix : 타입 수정 * Fix : 타입 수정 * Fix : 타입 수정 * Test : 테스트 추가 * Feat : 반환타입 변경 * 게시글 조회 메소드 작성 * @currentuser 커스텀 어노테이션 제작 * log 추가 * postType 에러 TC 작성 완료 * PostSingleResponseDto 작성 * Feat : 좋아요수 반환하는 메서드 추가 * Feat : 좋아요 엔드포인트 작성 * Feat : PresignedURL 발급 엔드포인트 작성 * Refactor : 메서드명 변경 * FIX : 반환타입 변경 * Feat : 뉴스 수정 엔드포인트 작성 * Feat : 뉴스 삭제 엔드포인트 작성 * feat:회원가입기본 * Feat : 커밋 누락 * feat:비밀번호 암호화 * Feat : Comment 서비스/레포지토리 작성 * Feat/Test : Comment 엔드포인트, 테스트 추가 * Feat/Test : 수정, 삭제 요청시 삭제하려는 댓글이 뉴스의 것이 맞는지 확인 * Chore : work * Fix : PathVariable value값 명시적으로 표시 * Fix : PathVariable value값 명시적으로 표시 * feat:이메일 인증 테스트 * refactor:리팩토링 * Fix : NoSuchElementException를 던져 전역예외처리기에서 404를 반환하도록 변경 * Fix : NoSuchElementException를 던져 전역예외처리기에서 404를 반환하도록 변경 * Feat : 예외처리 * Feat : 적절한 예외를 반환하도록 변경 * Feat : 적절한 예외를 반환하도록 변경 * Fix : News엔티티를 조회할 때 Member도 함께 조회 * fix:rq.getActor()에 ê°role추가 * rename: 멘토링레파지토리로 명칭 변경 * feat: JSON 태그를 List<String>으로 변환하는 컨버터 추가 * refactor: Json List 변환 로직 Ut.json로 추출 * feat: ErrorCode 인터페이스 추가 * feat: AccessDeniedException 핸들러 추가 * feat: member fixture 생성 * feat: mentoring fixture 생성 * feat: 멘토링 생성 * feat: 멘토링 수정 * feat: 멘토링 * feat: 멘토링 조회 * feat: 멘토링 목록 조회(페이징) * feat: 멘토 검색 * feat: 멘토링 검색 * test:로그인, 로그아웃 관련 테스트 * fix:redis관련 오류 일단 안보이게 * Feat#24 게시글 CRUD 기능 구현 완료 (#35) * [feat] 게시글 다건 조회 && 단건조회 TC 작성 && Init 데이터 설정 * [feat] 게시글 생성 유효성관련 테스트코드 추가 * [feat] 타입 별 저장 위치 분류 * [feat] Post 엔티티 관련 리팩토링 * 게시글 삭제 기능 구현 완료 * 게시글 좋아요 싫어요 기능 구현 완료 * 게시글 댓글 생성 기능 구현 * 게시글 CRUD 기능 구현 * feat:nickname추가 * fix: member 닉네임 필드 추가 반영(멘토링 관련) (#40) * member 관련 오류 해결 (#44) * Test : 생성자 변경 * feat:회원 í탈퇴 * [feat] Task 기능 구현 완료 (#48) * feat: Job/Task Repository, Service 구현 * feat: BaseInitData 추가, Job/Task 초기 데이터 생성 * feat: task 생성/매핑 로직 구현 * feat: Task 검색 기능 구현 * feat: Task 검색 테스트 작성 * feat: 관리자 기능 구현 * feat: 관리자 api 권한 확인 방법 변경 * feat: 삭제 api 추가 및 코드 중복 제거 * feat: 검색 기능 보완 * feat: 테스트 보강 * Feat#36 댓글 CRUID 기능 구현 (#52) * postComment 초기 데이터 작성 * 댓글 CRUD 기능 구현 * [Feat] 멘토 슬롯 등록, 수정 구현 (#53) * feat: 멘토 슬롯 생성 * feat: 멘토 슬롯 수정 * Docs: 멘토링 도메인 swagger 어노테이션 추가 (#58) * Test : 테스트 수정 (#59) * doc: Task 컨트롤러 api 문서 작성 (#60) * fix: TaskControllerTest 수정 (#63) * 페이징 처리 && 트랜젝션 추가 (#66) * feat:마이페이지 수정, 조회 (#69) * chore:swagger문서 (#74) * [Feat] 멘토 슬롯 도메인 조회, 삭제 구현 (#75) * Docs: 멘토링 api 문서 상세 작성 * refactor: 슬롯 수정 실패 테스트 추가 및 중복 제거 * Feat: 멘토 슬롯 삭제, 슬롯-예약 양방향 관계 설정 * Feat: 멘토 슬롯 조회 * feat: MentorSlotValidatorTest 추가 * refactor: Task 도메인 수정 (#76) * Refactor#67 중복 코드 리팩토링 및 N+1 문제 해결 (#77) * 좋아요 기능 수정(토글처리) * N+1 문제 해결 * 중복 코드 리팩토링 , 가독성 확보 * 파일업로드 및 트랜스코딩 파이프라인 작성 (#79) * Chore : 컨테이너 이름지정 삭제 * Feat : 카프카 컨테이너 작성 * Feat : 더 이상 버전 불필요 * Feat : 컨테이너 이름 및 플랫폼 지정 * Feat : 버전 다운 그레이드, ZooKeeper와 함께 사용하여 메시지 생성 소비 확인 * Fix : 업로드땐 HEAD 불필요 * Feat : ffmpeg 워커 추가 * Feat : ffmpeg 스크립트 수정필요 * Feat : 트랜스코딩을 위한 스크립트 작성 * Feat :sh/ffmpeg/transcode.py를 실행해 같은 폴더이 있는 input.mp4영상을 output.mp4영상으로 트랜스코딩하는 도커파일 작성 * Feat :카프카와 도커 컨테이너 토픽을 이용해 메시지 송수신 완료 * Feat : S3이벤트 수신완료 * Feat : S3이벤트 수신, 다운로드 완료 * Feat : ffmpeg설치 * Feat : 영상파일인지 판별, DASH인코딩 함수 추가 * Feat : 트랜스코딩 완료후 업로드 완료 * Feat : 화질별 트랜스코딩 완료 * Feat : 파이프라인 설정 완료 * Feat: 멘토 슬롯 반복 일정 생성 및 관리 (#81) * Feat: 멘토의 예약 가능 일정 목록 조회 * Feat: 멘토의 모든 일정 목록 조회 * Feat: 반복 슬롯 생성 * chore: 시크릿 키 노출 제거 (#87) --------- Co-authored-by: dooongdaeng <[email protected]> Co-authored-by: jiseopshin <[email protected]> Co-authored-by: sso0om <[email protected]> Co-authored-by: luckhee <[email protected]> Co-authored-by: 석희성 <[email protected]> Co-authored-by: 신지섭 <[email protected]>
1 parent 503fc2b commit 81b18eb

File tree

169 files changed

+10738
-99
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

169 files changed

+10738
-99
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,10 @@ out/
4141
CLAUDE.md
4242
db_dev.mv.db
4343
db_dev.trace.db
44+
45+
cookies.txt
46+
47+
.DS_Store
48+
49+
### QueryDSL Q클래스 ###
50+
**/generated/

Dockerfile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt .
6+
RUN apt-get update && apt-get install -y ffmpeg
7+
RUN pip install --no-cache-dir -r requirements.txt
8+
9+
COPY consumer.py .
10+
11+
ENV KAFKA_TOPIC=s3-events
12+
ENV KAFKA_BOOTSTRAP=kafka:9092
13+
ENV MINIO_ENDPOINT=http://minio:9000
14+
ENV MINIO_ACCESS_KEY=minioadmin
15+
ENV MINIO_SECRET_KEY=minioadmin
16+
ENV DOWNLOAD_DIR=/downloads
17+
18+
VOLUME ["/downloads"]
19+
20+
CMD ["python", "consumer.py"]

back/.env.default

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
CUSTOM__JWT__SECRET_KEY=NEED_TO_SET
1+
CUSTOM__JWT__SECRET_KEY=NEED_TO_SET

back/build.gradle.kts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import org.gradle.kotlin.dsl.implementation
2+
13
plugins {
24
java
35
id("org.springframework.boot") version "3.5.5"
46
id("io.spring.dependency-management") version "1.1.7"
57
}
68

7-
group = "com.ll"
9+
group = "com"
810
version = "0.0.1-SNAPSHOT"
911
description = "back"
1012

@@ -25,21 +27,45 @@ repositories {
2527
}
2628

2729
dependencies {
30+
// JWT
2831
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
2932
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
3033
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
34+
35+
// Swagger
3136
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9")
37+
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13")
38+
39+
// Spring Boot
3240
implementation("org.springframework.boot:spring-boot-starter-actuator")
3341
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
3442
implementation("org.springframework.boot:spring-boot-starter-security")
3543
implementation("org.springframework.boot:spring-boot-starter-web")
44+
implementation ("org.springframework.boot:spring-boot-starter-data-redis")
45+
46+
// QueryDSL
47+
implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
48+
annotationProcessor("com.querydsl:querydsl-apt:5.0.0:jakarta")
49+
annotationProcessor("jakarta.persistence:jakarta.persistence-api:3.1.0")
50+
annotationProcessor("jakarta.annotation:jakarta.annotation-api:2.1.1")
51+
52+
// Lombok
3653
compileOnly("org.projectlombok:lombok")
37-
developmentOnly("org.springframework.boot:spring-boot-devtools")
38-
runtimeOnly("com.h2database:h2")
3954
annotationProcessor("org.projectlombok:lombok")
55+
56+
// DB
57+
runtimeOnly("com.h2database:h2")
58+
runtimeOnly("com.mysql:mysql-connector-j")
59+
60+
// DevTools
61+
developmentOnly("org.springframework.boot:spring-boot-devtools")
62+
63+
// Test
4064
testImplementation("org.springframework.boot:spring-boot-starter-test")
4165
testImplementation("org.springframework.security:spring-security-test")
4266
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
67+
68+
implementation ("software.amazon.awssdk:s3:2.25.0")
4369
}
4470

4571
tasks.withType<Test> {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.back.domain.file.controller;
2+
3+
import com.back.domain.file.service.VideoService;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RequestParam;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
import java.net.URL;
10+
import java.util.List;
11+
import java.util.Map;
12+
13+
@RestController
14+
@RequiredArgsConstructor
15+
public class VideoController {
16+
17+
private final VideoService videoService;
18+
19+
// 업로드용 Presigned URL
20+
@GetMapping("/videos/upload-url")
21+
public URL getUploadUrl(@RequestParam String bucket, @RequestParam String fileName) {
22+
return videoService.generateUploadUrl(bucket, fileName);
23+
}
24+
25+
// DASH 스트리밍용 URL
26+
@GetMapping("/videos/dash-urls")
27+
public Map<String, URL> getDashUrls(
28+
@RequestParam String bucket,
29+
@RequestParam String mpdFile,
30+
@RequestParam List<String> segmentFiles
31+
) {
32+
return videoService.generateDashUrls(bucket, mpdFile, segmentFiles);
33+
}
34+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.back.domain.file.entity;
2+
3+
import com.back.global.jpa.BaseEntity;
4+
import jakarta.persistence.Column;
5+
import jakarta.persistence.Entity;
6+
import lombok.AccessLevel;
7+
import lombok.Builder;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
import org.hibernate.annotations.JdbcTypeCode;
11+
import org.hibernate.type.SqlTypes;
12+
13+
@Getter
14+
@Entity
15+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
16+
public class Video extends BaseEntity {
17+
@Column(unique = true)
18+
private String uuid;
19+
20+
@JdbcTypeCode(SqlTypes.JSON)
21+
@Column(name = "transcoding_results")
22+
private String transcodingResults;
23+
24+
private String originalPath;
25+
26+
private Integer views;
27+
28+
private String originalFileName;
29+
30+
private Integer duration;
31+
32+
private Long fileSize;
33+
34+
@Builder(access = AccessLevel.PRIVATE)
35+
private Video(String uuid, String transcodingResults, String originalPath, Integer views, String originalFileName, Integer duration, Long fileSize) {
36+
this.uuid = uuid;
37+
this.transcodingResults = transcodingResults;
38+
this.originalPath = originalPath;
39+
this.views = views;
40+
this.originalFileName = originalFileName;
41+
this.duration = duration;
42+
this.fileSize = fileSize;
43+
}
44+
45+
public static Video create(String uuid, String transcodingResults, String originalPath, String originalFileName, Integer duration, Long fileSize) {
46+
if (uuid == null || uuid.isBlank()) {
47+
throw new IllegalArgumentException("uuid cannot be null or empty");
48+
}
49+
if (originalPath == null || originalPath.isBlank()) {
50+
throw new IllegalArgumentException("originalPath cannot be null or empty");
51+
}
52+
if (originalFileName == null || originalFileName.isBlank()) {
53+
throw new IllegalArgumentException("originalFileName cannot be null or empty");
54+
}
55+
56+
return Video.builder()
57+
.uuid(uuid)
58+
.transcodingResults(transcodingResults)
59+
.originalPath(originalPath)
60+
.views(0)
61+
.originalFileName(originalFileName)
62+
.duration(duration)
63+
.fileSize(fileSize)
64+
.build();
65+
}
66+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.back.domain.file.repository;
2+
3+
import com.back.domain.file.entity.Video;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
import java.util.Optional;
8+
9+
@Repository
10+
public interface VideoRepository extends JpaRepository<Video, Integer> {
11+
Optional<Video> findByUuid(String uuid);
12+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.back.domain.file.service;
2+
3+
import com.back.domain.file.entity.Video;
4+
import com.back.domain.file.repository.VideoRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Service;
7+
import software.amazon.awssdk.services.s3.S3Client;
8+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
9+
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
10+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
11+
import software.amazon.awssdk.services.s3.model.S3Exception;
12+
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
13+
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
14+
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
15+
16+
import java.net.URL;
17+
import java.time.Duration;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.NoSuchElementException;
21+
import java.util.UUID;
22+
import java.util.stream.Collectors;
23+
24+
@Service
25+
@RequiredArgsConstructor
26+
public class VideoService {
27+
private final VideoRepository videoRepository;
28+
private final S3Presigner presigner;
29+
private final S3Client s3Client;
30+
31+
public Video createVideo(String transcodingStatus, String originalPath, String originalFilename, Integer duration, Long fileSize) {
32+
String uuid = UUID.randomUUID().toString();
33+
Video video = Video.create(uuid, transcodingStatus, originalPath, originalFilename, duration, fileSize);
34+
return videoRepository.save(video);
35+
}
36+
37+
public Video getNewsByUuid(String uuid) {
38+
return videoRepository.findByUuid(uuid)
39+
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 비디오입니다."));
40+
}
41+
42+
//HeadObjectRequest 고려
43+
public URL generateUploadUrl(String bucket, String objectKey) {
44+
PutObjectRequest request = PutObjectRequest.builder()
45+
.bucket(bucket)
46+
.key(objectKey)
47+
.build();
48+
49+
PresignedPutObjectRequest presignedRequest =
50+
presigner.presignPutObject(builder -> builder
51+
.signatureDuration(Duration.ofMinutes(30))
52+
.putObjectRequest(request));
53+
54+
URL url = presignedRequest.url();
55+
if (url == null) {
56+
throw new RuntimeException("Presigned URL 생성 실패");
57+
}
58+
59+
return url;
60+
}
61+
62+
public URL generateDownloadUrl(String bucket, String objectKey) {
63+
if(!isExist(bucket, objectKey)){
64+
throw new NoSuchElementException("요청한 파일이 존재하지 않습니다: " + objectKey);
65+
}
66+
67+
GetObjectRequest request = GetObjectRequest.builder()
68+
.bucket(bucket)
69+
.key(objectKey)
70+
.build();
71+
72+
PresignedGetObjectRequest presignedRequest =
73+
presigner.presignGetObject(builder -> builder
74+
.signatureDuration(Duration.ofHours(1))
75+
.getObjectRequest(request));
76+
77+
URL url = presignedRequest.url();
78+
if (url == null) {
79+
throw new RuntimeException("Presigned URL 생성 실패");
80+
}
81+
82+
return url;
83+
}
84+
85+
// DASH용 인덱스 + 세그먼트 URL 발급
86+
public Map<String, URL> generateDashUrls(String bucket, String mpdFile, List<String> segmentFiles) {
87+
// MPD 파일 URL
88+
URL mpdUrl = generateDownloadUrl(bucket, mpdFile);
89+
90+
// 각 세그먼트 파일 URL
91+
Map<String, URL> segmentUrls = segmentFiles.stream()
92+
.collect(Collectors.toMap(f -> f, f -> generateDownloadUrl(bucket, f)));
93+
94+
// MPD 포함 합쳐서 반환
95+
segmentUrls.put("mpd", mpdUrl);
96+
return segmentUrls;
97+
}
98+
99+
public boolean isExist(String bucket, String objectKey) {
100+
try {
101+
HeadObjectRequest headRequest = HeadObjectRequest.builder()
102+
.bucket(bucket)
103+
.key(objectKey)
104+
.build();
105+
106+
s3Client.headObject(headRequest);
107+
return true;
108+
} catch (S3Exception e) {
109+
return false;
110+
}
111+
}
112+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.back.domain.job.job.entity;
2+
3+
import com.back.global.jpa.BaseEntity;
4+
import jakarta.persistence.*;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
import lombok.Setter;
8+
9+
import java.util.List;
10+
11+
@Entity
12+
@Table(name = "job")
13+
@Getter @Setter
14+
@NoArgsConstructor
15+
public class Job extends BaseEntity {
16+
@Column(name = "name", nullable = false, unique = true)
17+
private String name;
18+
19+
@Column(name = "description", columnDefinition = "TEXT")
20+
private String description;
21+
22+
@OneToMany(mappedBy = "job", cascade = CascadeType.ALL)
23+
private List<JobAlias> aliases;
24+
25+
public Job(String name, String description) {
26+
this.name = name;
27+
this.description = description;
28+
}
29+
30+
public void addAlias(JobAlias alias) {
31+
aliases.add(alias);
32+
alias.setJob(this);
33+
}
34+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.back.domain.job.job.entity;
2+
3+
import com.back.global.jpa.BaseEntity;
4+
import jakarta.persistence.*;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
import lombok.Setter;
8+
9+
@Entity
10+
@Table(name = "job_alias")
11+
@Getter @Setter
12+
@NoArgsConstructor
13+
public class JobAlias extends BaseEntity {
14+
@Column(name = "name", nullable = false, unique = true)
15+
private String name; // 사용자가 입력한 직군 이름
16+
17+
@ManyToOne(fetch = FetchType.LAZY)
18+
@JoinColumn(name = "job_id")
19+
private Job job; // 표준 Job 연결 (NULL이면 pending 상태)
20+
21+
public JobAlias(String name) {
22+
this.name = name;
23+
this.job = null; // 기본적으로 연결된 Job이 없음 (pending 상태)
24+
}
25+
}

0 commit comments

Comments
 (0)