Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ dependencies {
// Email
implementation 'org.springframework.boot:spring-boot-starter-mail'

// Github Webhook
implementation 'org.kohsuke:github-api:1.327'

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.teamEWSN.gitdeun.Application.entity;

import com.teamEWSN.gitdeun.Recruitment.entity.Recruitment;
import com.teamEWSN.gitdeun.Recruitment.entity.RecruitmentField;
import com.teamEWSN.gitdeun.common.util.AuditedEntity;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "application",
uniqueConstraints = {
// 동일 μ‚¬μš©μžΒ·κ³΅κ³ μ— λŒ€ν•΄ "ν™œμ„±" μ‹ μ²­ 쀑볡 λ°©μ§€μš© (WITHDRAWN μ œμ™Έ)
@UniqueConstraint(name = "uk_active_application",
columnNames = {"recruitment_id", "applicant_id", "active"})
},
indexes = {
@Index(name = "idx_application_recruitment", columnList = "recruitment_id"),
@Index(name = "idx_application_applicant", columnList = "applicant_id"),
@Index(name = "idx_application_status", columnList = "status")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Application extends AuditedEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/** λͺ¨μ§‘곡고 */
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "recruitment_id", nullable = false)
private Recruitment recruitment;

/** μ‹ μ²­ μ‚¬μš©μž (User.id) */
@Column(name = "applicant_id", nullable = false)
private Long applicantId;

/** 지원 λΆ„μ•Ό (μž‘μ„±μžμ˜ λͺ¨μ§‘ λΆ„μ•Ό μ€‘μ—μ„œ 선택) */
@Enumerated(EnumType.STRING)
@Column(name = "applied_field", nullable = false, length = 32)
private RecruitmentField appliedField;

/** 지원 λ©”μ„Έμ§€ */
@Column(name = "message", length = 1000)
private String message;

/** μ‹ μ²­ 거절 μ‚¬μœ (거절 μ‹œ 선택) */
@Column(name = "reject_reason", length = 500)
private String rejectReason;

/** μ‹ μ²­ μƒνƒœ (κ²€ν†  쀑 / 수락 / 거절 / 철회) */
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 24)
private ApplicationStatus status;

/** ν™œμ„± μ‹ μ²­ μ—¬λΆ€ (WITHDRAWN λ©΄ false) */
@Column(nullable = false)
private boolean active;


// μƒνƒœ 전이
public void accept() { this.status = ApplicationStatus.ACCEPTED; }
public void reject(String reason) { this.status = ApplicationStatus.REJECTED; this.rejectReason = reason; }
public void withdraw() { this.status = ApplicationStatus.WITHDRAWN; this.active = false; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.teamEWSN.gitdeun.Application.entity;

public enum ApplicationStatus {
PENDING, // 검토쀑
ACCEPTED, // 수락
REJECTED, // 거절
WITHDRAWN // 철회
}
110 changes: 110 additions & 0 deletions src/main/java/com/teamEWSN/gitdeun/Recruitment/entity/Recruitment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.teamEWSN.gitdeun.Recruitment.entity;

import com.teamEWSN.gitdeun.Application.entity.Application;
import com.teamEWSN.gitdeun.userskill.entity.DeveloperSkillEnum;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Entity
@Table(name = "recruitment",
indexes = {
@Index(name = "idx_recruitment_status", columnList = "status"),
@Index(name = "idx_recruitment_deadline", columnList = "end_at"),
@Index(name = "idx_recruitment_recruiter", columnList = "recruiter_id")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Recruitment {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/** λͺ¨μ§‘μž (μžλ™ 선택, User.id) */
@Column(name = "Recruiter_id", nullable = false)
private Long RecruiterId;

/** λͺ¨μ§‘ 곡고 제λͺ© (μž…λ ₯ ν•„μš”) */
@Column(nullable = false, length = 120)
private String title;

/** 연락망: 이메일 μ£Όμ†Œ (μž…λ ₯ ν•„μš”) */
@Column(length = 120)
private String contactEmail;

/** λͺ¨μ§‘일 (μž…λ ₯ ν•„μš”) */
@Column(name = "start_at", nullable = false)
private LocalDateTime startAt;

// λͺ¨μ§‘ 마감일 (μž…λ ₯ ν•„μš”)
@Column(name = "end_at", nullable = false)
private LocalDateTime endAt;

/** λͺ¨μ§‘ λ‚΄μš© (μž…λ ₯ ν•„μš”) */
@Lob
@Column(nullable = false)
private String content;

/** νŒ€ 규λͺ¨(총인원) (μž…λ ₯ ν•„μš”)*/
@Column(name = "team_size_total", nullable = false)
private Integer teamSizeTotal;

/** 남은 λͺ¨μ§‘ 인원(μž…λ ₯ ν•„μš”) – μ‹ μ²­ μ‹œ 1 κ°μ†Œ, 철회/거절 μ‹œ 1 증가(볡원) */
@Column(name = "recruit_quota", nullable = false)
private Integer recruitQuota;

/** 개발 μ–Έμ–΄ νƒœκ·Έ (선택 ν•„μš”) - ν™”λ©΄ ν•„ν„°/ν‘œμ‹œμš© */
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "recruitment_language_tags", joinColumns = @JoinColumn(name = "recruitment_id"))
@Column(name = "language", nullable = false, length = 64)
@Enumerated(EnumType.STRING)
@Builder.Default
private Set<DeveloperSkillEnum> languageTags = new HashSet<>();

/** 개발 λΆ„μ•Ό νƒœκ·Έ (선택 ν•„μš”) - BACKEND/FRONTEND/AI λ“± */
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "recruitment_field_tags", joinColumns = @JoinColumn(name = "recruitment_id"))
@Column(name = "field", nullable = false, length = 32)
@Enumerated(EnumType.STRING)
@Builder.Default
private Set<RecruitmentField> fieldTags = new HashSet<>();

/** λͺ¨μ§‘ μƒνƒœ (λͺ¨μ§‘ μ˜ˆμ • / λͺ¨μ§‘ 쀑 / λͺ¨μ§‘ 마감) */
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 24)
private RecruitmentStatus status;

/** 쑰회수 */
@Column(name = "view_count", nullable = false)
@Builder.Default
private Long viewCount = 0L;

/** λͺ¨μ§‘ 곡고 이미지 (선택) */
@Builder.Default
@OneToMany(mappedBy = "recruitment", cascade = CascadeType.ALL, orphanRemoval = true)
private List<RecruitmentImage> recruitmentImages = new ArrayList<>();

/** 지원 μ‹ μ²­ 리슀트 (μ–‘λ°©ν–₯ ν•„μš”μ‹œ λ§€ν•‘) */
@OneToMany(mappedBy = "recruitment", fetch = FetchType.LAZY)
@Builder.Default
private List<Application> applications = new ArrayList<>();

/** μΆ”μ²œ κ°€μ€‘μΉ˜μš© μš”κ΅¬ 기술(μ–Έμ–΄/ν”„λ ˆμž„μ›Œν¬/툴 λ“±, 선택) */
@OneToMany(mappedBy = "recruitment", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private Set<RecruitmentRequiredSkill> requiredSkills = new HashSet<>();



public void increaseView() { this.viewCount++; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.teamEWSN.gitdeun.Recruitment.entity;

public enum RecruitmentField {
FRONTEND, // ν”„λ‘ νŠΈμ—”λ“œ
BACKEND, // λ°±μ—”λ“œ
FULLSTACK, // ν’€μŠ€νƒ
ANDROID, // μ•ˆλ“œλ‘œμ΄λ“œ
IOS, // iOS
DATA, // 데이터 뢄석
DEVOPS, // DevOps
AI, // 인곡지λŠ₯
EMBEDDED, // μž„λ² λ””λ“œ
GAME, // κ²Œμž„
SECURITY, // λ³΄μ•ˆ
ETC; // 기타
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.teamEWSN.gitdeun.Recruitment.entity;

import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDateTime;

@Entity
@Getter
@Setter
@Builder
@Table(name = "recruitment_image", indexes = {
@Index(name = "idx_recruitment_image_recruitment_id_deleted_at", columnList = "recruitment_id, deleted_at")
})
@NoArgsConstructor
@AllArgsConstructor
public class RecruitmentImage {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "recruitment_id")
private Recruitment recruitment;

@Column(name = "image_url", length = 255)
private String imageUrl;

@Column(name = "deleted_at")
private LocalDateTime deletedAt;

// Soft delete 처리 λ©”μ†Œλ“œ
public void softDelete() {
this.deletedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.teamEWSN.gitdeun.Recruitment.entity;

import com.teamEWSN.gitdeun.userskill.entity.DeveloperSkillEnum;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "recruitment_required_skill",
uniqueConstraints = @UniqueConstraint(name = "uk_recruit_skill", columnNames = {"recruitment_id","skill"}),
indexes = {
@Index(name = "idx_recruit_skill_recruitment", columnList = "recruitment_id"),
@Index(name = "idx_recruit_skill_category", columnList = "category"),
@Index(name = "idx_rrs_cat_weight_skill", columnList = "category, weight, skill")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RecruitmentRequiredSkill {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "recruitment_id", nullable = false)
private Recruitment recruitment;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 64)
private DeveloperSkillEnum skill;

@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 32)
private SkillCategory category; // μΆ”μ²œμ€ LANGUAGE만 μ‚¬μš©

@Column(nullable = false)
@Builder.Default
private double weight = 1.0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.teamEWSN.gitdeun.Recruitment.entity;

public enum RecruitmentStatus {
Forthcoming, // λͺ¨μ§‘ μ˜ˆμ •
RECRUITING, // λͺ¨μ§‘ 쀑
CLOSED, // 마감
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.teamEWSN.gitdeun.Recruitment.entity;

public enum SkillCategory {
LANGUAGE,
FRAMEWORK,
DATABASE,
TOOLS,
ETC
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// κ²½λ‘œλ³„ 인가 μž‘μ—…
http
.authorizeHttpRequests((auth) -> auth
// λ‚΄λΆ€ webhook 톡신 API
.requestMatchers("/api/webhook/**").permitAll()
// μ™ΈλΆ€ 곡개 API(ν΄λΌμ΄μ–ΈνŠΈ - JWT)
.requestMatchers(SecurityPath.ADMIN_ENDPOINTS).hasRole("ADMIN")
.requestMatchers(SecurityPath.USER_ENDPOINTS).hasAnyRole("USER", "ADMIN")
.requestMatchers(SecurityPath.PUBLIC_ENDPOINTS).permitAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class SecurityPath {
"/api/repos/**",
"/api/mindmaps/**",
"/api/history/**",
"/api/invitations/**",
"/api/notifications/**",
"/api/s3/bucket/**",
"/api/proxy/**"
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public enum ErrorCode {
SOCIAL_CONNECTION_NOT_FOUND(HttpStatus.NOT_FOUND, "ACCOUNT-004", "μ—°λ™λœ μ†Œμ…œ 계정 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),
USER_SETTING_NOT_FOUND_BY_ID(HttpStatus.NOT_FOUND, "ACCOUNT-005", "ν•΄λ‹Ή μ•„μ΄λ””μ˜ 섀정을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),

// 개발 기술 κ΄€λ ¨
SKILL_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "SKILL-001", "μ΅œλŒ€ 10κ°œκΉŒμ§€λ§Œ 선택 κ°€λŠ₯ν•©λ‹ˆλ‹€."),
INVALID_SKILL(HttpStatus.BAD_REQUEST, "SKILL-002", "μœ νš¨ν•˜μ§€ μ•Šμ€ κΈ°μˆ μž…λ‹ˆλ‹€."),

// μ†Œμ…œ 둜그인 κ΄€λ ¨
OAUTH_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "OAUTH-001", "μ†Œμ…œ 둜그인 처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."),
UNSUPPORTED_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "OAUTH-002", "μ§€μ›ν•˜μ§€ μ•ŠλŠ” μ†Œμ…œ 둜그인 μ œκ³΅μžμž…λ‹ˆλ‹€."),
Expand All @@ -44,6 +48,7 @@ public enum ErrorCode {
// 멀버 κ΄€λ ¨
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER-001", "ν•΄λ‹Ή 멀버λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),
MEMBER_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER-002", "이미 λ§ˆμΈλ“œλ§΅μ— λ“±λ‘λœ λ©€λ²„μž…λ‹ˆλ‹€."),
OWNER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER-003", "ν•΄λ‹Ή μ†Œμœ μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),

// μ΄ˆλŒ€ κ΄€λ ¨
INVITATION_NOT_FOUND(HttpStatus.NOT_FOUND, "INVITE-001", "μ΄ˆλŒ€ 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."),
Expand Down
Loading