Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
88d9aa0
feat: Authority 변경 API 추가
rlajm1203 Aug 23, 2025
a61cef2
feat: findRole 메소드 추가
rlajm1203 Aug 23, 2025
a9fe88a
feat: findByIdAndRole 메소드 추가
rlajm1203 Aug 23, 2025
ceb4af5
feat: 사용자의 권한을 변경하는 로직 추가
rlajm1203 Aug 23, 2025
d724fec
feat: 사용자의 권한을 찾을 수 없을 경우의 예외 정의
rlajm1203 Aug 23, 2025
7b97540
feat: isExist 메소드 추가
rlajm1203 Aug 23, 2025
20cbf12
feat: AuthorityUpdateRequest 정의
rlajm1203 Aug 23, 2025
85060c2
feat: authority 수정 api 구현
rlajm1203 Aug 23, 2025
51b4b2a
refactor: RoleType 추가
rlajm1203 Aug 23, 2025
39b067b
refactor: 회원가입 시에 생성하는, 기본 Authority 생성 추가
rlajm1203 Aug 23, 2025
7816361
refactor: 잘못된 Type 수정
rlajm1203 Aug 23, 2025
9fcb02c
feat: Authority 리스트 조회 API 추가
rlajm1203 Aug 23, 2025
a2831ce
feat: 어노테이션 제거, 생성자 추가
rlajm1203 Aug 23, 2025
e68879c
delete: Authority 삭제
rlajm1203 Aug 23, 2025
498ac05
refactor: Authority 삭제
rlajm1203 Aug 24, 2025
4064b26
feat: Department Enum 정의
rlajm1203 Aug 24, 2025
3c82782
feat: Department 수정 Request, Response 정의
rlajm1203 Aug 24, 2025
2b519ba
feat: DepartmentUsecase, DepartmentService 구현
rlajm1203 Aug 24, 2025
846efdc
faet: MemberModel에 Department 추가
rlajm1203 Aug 24, 2025
fb52fd6
feat: Department 수정 API 구현 및 시큐리티 설정 추가
rlajm1203 Aug 24, 2025
abe3989
feat: koName 추가
rlajm1203 Aug 24, 2025
f4087e1
feat: enName 으로 필드명 수정
rlajm1203 Aug 24, 2025
d4b516f
refactor: DepartmentResponse에 koName, enName 추가
rlajm1203 Aug 24, 2025
753d37e
feat: NotFoundDepartmentException 정의
rlajm1203 Aug 24, 2025
0dba966
refactor: NotFoundDepartmentException 을 던지도록 수정
rlajm1203 Aug 24, 2025
30a9cc3
refactor: Department 조회 메소드 수정
rlajm1203 Aug 24, 2025
52514e4
refactor: save 호출
rlajm1203 Aug 24, 2025
75495fd
refactor: 활동 상태 조회 응답에 department 추가
rlajm1203 Aug 24, 2025
850b9c4
style: spotlessApply
rlajm1203 Aug 24, 2025
6fbc5da
feat: swagger 명세 추가
rlajm1203 Aug 24, 2025
be1efc1
refactor: 부서 리스트 조회는, USER 권한으로도 가능하게 수정
rlajm1203 Aug 24, 2025
d875f34
delete: 필요 없는 dto 삭제
rlajm1203 Aug 24, 2025
2e5c68d
refactor: 회원 부서 수정 API QueryString 으로 수정
rlajm1203 Aug 24, 2025
975f973
style: spotlessApply
rlajm1203 Aug 24, 2025
606abd3
refactor: 대소문자 구분하지 않도록 수정
rlajm1203 Aug 24, 2025
965e95b
style: spotlessApply
rlajm1203 Aug 24, 2025
243f476
refactor: securityFilterChain 수정
rlajm1203 Aug 24, 2025
1e1b6b7
refactor: 메소드 이름 변경
rlajm1203 Aug 24, 2025
36d72dd
refactor: 트랜잭션 추가
rlajm1203 Aug 24, 2025
0833a8b
refactor: find 메소드 유효성 검증 로직 추가
rlajm1203 Aug 24, 2025
6791573
refactor: getMemberId() 메소드 호출로 변경
rlajm1203 Aug 24, 2025
cac1d13
refactor: 테이블 유니크 제약조건 추가
rlajm1203 Aug 24, 2025
e6f91ea
refactor: 부서 응답 lowercase 로 수정
rlajm1203 Aug 24, 2025
388cec3
style: spotlessApply
rlajm1203 Aug 24, 2025
c084229
refactor: equalsIgnoreCase 로 변경
rlajm1203 Aug 24, 2025
ad1e714
style: spotlessApply
rlajm1203 Aug 24, 2025
d21e24c
[FEAT] 캘린더 기능을 추가합니다. (#281)
rlajm1203 Sep 21, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.blackcompany.eeos.auth.application.exception;

import com.blackcompany.eeos.common.exception.BusinessException;
import org.springframework.http.HttpStatus;

public class NotFoundAuthorityException extends BusinessException {

private static final String FAIL_CODE = "4013";

public NotFoundAuthorityException() {
super(FAIL_CODE, HttpStatus.NOT_FOUND);
}

@Override
public String getMessage() {
return "회원의 권한을 찾을 수 없습니다.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@
import com.blackcompany.eeos.common.support.AbstractModel;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
@Builder
public class AuthorityModel implements AbstractModel {

private final Long id;
private final Long memberId;
private final Role role;
private Role role;

public AuthorityModel(Long id, Long memberId, Role role) {
this.id = id;
this.memberId = memberId;
this.role = role;
}

public void updateRole(Role role) {
this.role = role;
}

public static AuthorityModel create(Long memberId, Role role) {
return AuthorityModel.builder().role(role).memberId(memberId).build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,45 @@
package com.blackcompany.eeos.auth.application.model;

import java.util.Arrays;
import java.util.List;
import lombok.Getter;

@Getter
public enum Role {
ROLE_ADMIN("ADMIN"),
ROLE_USER("USER");
ROLE_ADMIN(1L, "ADMIN"),
ROLE_USER(2L, "USER");

private String role;
private final Long id;
private final String role; // 시스템 내에 존재하는 role을 찾을 때, 이 문자열을 기준으로 찾습니다.

Role(String role) {
Role(Long id, String role) {
this.id = id;
this.role = role;
}

public static boolean isExist(String role) {
if (role == null || role.trim().isEmpty()) return false;
return Arrays.stream(Role.values()).anyMatch(obj -> obj.getRole().equalsIgnoreCase(role));
}

public static Role findRole(String role) {
if (role == null || role.trim().isEmpty()) {
throw new IllegalArgumentException("해당 role 을 찾을 수 없습니다.");
}
return Arrays.stream(Role.values())
.filter(obj -> obj.getRole().equalsIgnoreCase(role))
.findFirst()
.orElseThrow(IllegalArgumentException::new);
}

public static Role findById(Long id) {
return Arrays.stream(Role.values())
.filter(obj -> obj.getId().equals(id))
.findFirst()
.orElseThrow(IllegalArgumentException::new);
}

public static List<Role> getAllRoles() {
return Arrays.asList(Role.values());
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.blackcompany.eeos.auth.application.repository;

import com.blackcompany.eeos.auth.application.model.AuthorityModel;
import com.blackcompany.eeos.auth.application.model.Role;
import java.util.Set;

public interface AuthorityRepository {

Set<AuthorityModel> findByMemberId(Long memberId);

AuthorityModel findByMemberIdAndRole(Long memberId, Role role);

Long save(AuthorityModel authorityModel);
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private OauthMemberModel signUpMember(final OauthMemberModel model) {
.build();
MemberModel savedMember = memberRepository.save(member);

authorityRepository.save(AuthorityModel.create(savedMember.getMemberId(), Role.ROLE_USER));
createDefaultRole(savedMember.getMemberId());

OauthMemberModel updatedModel = model.toBuilder().memberId(savedMember.getId()).build();
return oAuthMemberRepository.save(updatedModel);
Expand All @@ -68,4 +68,8 @@ private OauthMemberModel signUpMember(final OauthMemberModel model) {
private void checkPassword(String password, String encryptedPassword) {
if (!encryptHelper.isMatch(password, encryptedPassword)) throw new NotFoundAccountException();
}

private void createDefaultRole(Long memberId) {
authorityRepository.save(AuthorityModel.create(memberId, Role.ROLE_USER));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public TokenModel signUp(AdditionalInfoApplicationCommand command) {

// TODO: 일반 USER 권한인지 아닌지 계산해주는 도구 추가

return tokenGenerator.execute(savedMember.getId());
return tokenGenerator.execute(savedMember.getMemberId());
}

private void saveOAuth(String oAuthId, Long memberId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -25,7 +26,11 @@
@SuperBuilder(toBuilder = true)
@Table(
name = AuthorityEntity.NAME,
indexes = {@Index(name = "idx_member_id", columnList = AuthorityEntity.NAME + "_member_id")})
indexes = {@Index(name = "idx_member_id", columnList = AuthorityEntity.NAME + "_member_id")},
uniqueConstraints =
@UniqueConstraint(
name = "uk_authority_member_role",
columnNames = {AuthorityEntity.NAME + "_member_id", AuthorityEntity.NAME + "_role"}))
Comment on lines +29 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

(member_id, role) 유니크 제약 추가: 스키마 정합성/마이그레이션 확인 필요

JPA 레벨에서 중복 방지에 유효합니다. 다만 운영 DB에 중복 데이터가 존재하면 마이그레이션 시 실패합니다. 배포 전 중복 정리 및 DDL 적용 스크립트 존재 여부를 확인해 주세요.

레포 내 마이그레이션에 본 제약이 포함되어 있는지 검색:

운영 DB 점검용 SQL 예시(참고):

-- 중복 후보
SELECT authority_member_id, authority_role, COUNT(*) 
FROM authority
GROUP BY authority_member_id, authority_role
HAVING COUNT(*) > 1;

🏁 Script executed:

#!/bin/bash
# 마이그레이션 파일 내 제약 이름 검색
rg -n "uk_authority_member_role" -S
# authority 테이블 관련 마이그레이션 후보도 같이 탐색
rg -nP -C2 '\bauthority\b.*(unique|constraint|index)' -S

Length of output: 227


마이그레이션에 uk_authority_member_role 제약 미포함 확인됨: DDL 스크립트 추가 필요

검색 결과 AuthorityEntity.java(29–33줄)에만 JPA 어노테이션으로 UNIQUE 제약이 선언되어 있고, 마이그레이션 스크립트(예: Flyway/Liquibase) 내에는 해당 제약을 적용하는 구문을 찾을 수 없습니다.
운영 DB에 중복 데이터가 남아 있으면 배포 시 마이그레이션이 실패할 수 있으므로, 아래 작업을 반드시 수행해 주세요:

  • migration 스크립트에 다음과 같은 구문 추가
    ALTER TABLE authority
      ADD CONSTRAINT uk_authority_member_role UNIQUE (authority_member_id, authority_role);
  • 배포 전 운영 DB에서 중복 레코드 조회 및 정리
    SELECT authority_member_id, authority_role, COUNT(*) AS cnt
    FROM authority
    GROUP BY authority_member_id, authority_role
    HAVING COUNT(*) > 1;

작업 완료 후, 마이그레이션 파일이 정상 적용되는지 재확인 부탁드립니다.

🤖 Prompt for AI Agents
eeos/src/main/java/com/blackcompany/eeos/auth/persistence/authority/AuthorityEntity.java
(lines 29-33): the JPA annotation declares a UNIQUE constraint
uk_authority_member_role but there is no corresponding migration; add a new
migration (Flyway/Liquibase) that issues an ALTER TABLE to add a UNIQUE
constraint named uk_authority_member_role on columns authority_member_id and
authority_role, before applying it run a query on the production DB to find and
remove any duplicate (member_id, role) rows so the ALTER TABLE will succeed, and
verify the migration applies cleanly in a pre-prod environment before
merging/deploying.

@Getter
public class AuthorityEntity extends BaseEntity {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.blackcompany.eeos.auth.persistence.authority;

import com.blackcompany.eeos.auth.application.model.Role;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
Expand All @@ -9,4 +11,8 @@ public interface AuthorityJpaRepository extends JpaRepository<AuthorityEntity, L

@Query("SELECT DISTINCT a FROM AuthorityEntity a WHERE a.memberId = :memberId")
Set<AuthorityEntity> findByMemberId(@Param("memberId") Long memberId);

@Query("SELECT a FROM AuthorityEntity a WHERE a.memberId=:memberId AND a.role=:role")
Optional<AuthorityEntity> findByMemberIdAndRole(
@Param("memberId") Long memberId, @Param("role") Role role);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.blackcompany.eeos.auth.persistence.authority;

import com.blackcompany.eeos.auth.application.exception.NotFoundAuthorityException;
import com.blackcompany.eeos.auth.application.model.AuthorityModel;
import com.blackcompany.eeos.auth.application.model.Role;
import com.blackcompany.eeos.auth.application.repository.AuthorityRepository;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -20,6 +22,14 @@ public Set<AuthorityModel> findByMemberId(Long memberId) {
.collect(Collectors.toSet());
}

@Override
public AuthorityModel findByMemberIdAndRole(Long memberId, Role role) {
return repository
.findByMemberIdAndRole(memberId, role)
.map(this::toModel)
.orElseThrow(NotFoundAuthorityException::new);
}

@Override
public Long save(AuthorityModel authorityModel) {
return repository.save(toEntity(authorityModel)).getId();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.blackcompany.eeos.calendar.application.dto;

import com.blackcompany.eeos.common.support.dto.AbstractRequestDto;

public record CalendarCreateCommand(String title, String url, String type, Long startAt, Long endAt)
implements AbstractRequestDto {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.blackcompany.eeos.calendar.application.dto;

public record CalendarQuery(Integer year, Integer month, Integer date, Integer duration) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.blackcompany.eeos.calendar.application.dto;

import com.blackcompany.eeos.calendar.application.model.CalendarModel;
import com.blackcompany.eeos.common.support.dto.AbstractResponseDto;
import com.blackcompany.eeos.common.utils.DateConverter;

public record CalendarResponse(
Long id, String title, String url, String type, Long startAt, Long endAt, String writer)
implements AbstractResponseDto {

public static CalendarResponse toResponse(CalendarModel model, String writerName) {
Long startAt = DateConverter.toMillis(model.getStartAt());
Long endAt = DateConverter.toMillis(model.getEndAt());
String type = model.getType().name();

return new CalendarResponse(
model.getId(), model.getTitle(), model.getUrl(), type, startAt, endAt, writerName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.blackcompany.eeos.calendar.application.dto;

import com.blackcompany.eeos.common.support.dto.AbstractResponseDto;
import java.util.List;

public record CalendarResponses(List<CalendarResponse> calendars) implements AbstractResponseDto {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.blackcompany.eeos.calendar.application.dto;

import com.blackcompany.eeos.common.support.dto.AbstractRequestDto;

public record CalendarUpdateCommand(String title, String url, String type, Long startAt, Long endAt)
implements AbstractRequestDto {}
Comment on lines +5 to +6
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

입력 유효성 보강(필드 제약·교차 검증)과 타입 명확화가 필요합니다.

  • 제목/타입은 공란 불가, URL 길이/형식 제한, 시간 필수 및 startAt ≤ endAt 보장이 필요합니다.
  • 가능하면 type은 String 대신 Enum(예: CalendarType)으로 받고, 시간은 Instant/LocalDateTime로 받는 것을 권장합니다.

예시:

+import jakarta.validation.constraints.*;
+import org.hibernate.validator.constraints.URL;
-public record CalendarUpdateCommand(String title, String url, String type, Long startAt, Long endAt)
-        implements AbstractRequestDto {}
+public record CalendarUpdateCommand(
+    @NotBlank @Size(max = 100) String title,
+    @Size(max = 2048) @URL(regexp = "^(https?://).*$", message = "유효한 URL이어야 합니다") String url,
+    @NotBlank String type,
+    @NotNull Long startAt,
+    @NotNull Long endAt
+) implements AbstractRequestDto {
+    public CalendarUpdateCommand {
+        if (startAt != null && endAt != null && startAt > endAt) {
+            throw new IllegalArgumentException("startAt은 endAt보다 이후일 수 없습니다");
+        }
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public record CalendarUpdateCommand(String title, String url, String type, Long startAt, Long endAt)
implements AbstractRequestDto {}
import jakarta.validation.constraints.*;
import org.hibernate.validator.constraints.URL;
public record CalendarUpdateCommand(
@NotBlank @Size(max = 100) String title,
@Size(max = 2048) @URL(regexp = "^(https?://).*$", message = "유효한 URL이어야 합니다") String url,
@NotBlank String type,
@NotNull Long startAt,
@NotNull Long endAt
) implements AbstractRequestDto {
public CalendarUpdateCommand {
if (startAt != null && endAt != null && startAt > endAt) {
throw new IllegalArgumentException("startAt은 endAt보다 이후일 수 없습니다");
}
}
}
🤖 Prompt for AI Agents
In
eeos/src/main/java/com/blackcompany/eeos/calendar/application/dto/CalendarUpdateCommand.java
around lines 5-6, the DTO currently exposes raw Strings and Longs without
validation; change the API to use a CalendarType enum for the type and a
temporal type (Instant or LocalDateTime) for start/end, add field-level
constraints (e.g., @NotBlank for title and type, @Size and URL/regex constraint
for url, @NotNull for times) and implement cross-field validation to enforce
startAt ≤ endAt (either an @AssertTrue method or a custom class-level
ConstraintValidator). Ensure the enum CalendarType is added to the model,
convert/deserialize incoming values appropriately, and surface validation errors
via standard Bean Validation so invalid inputs are rejected before business
logic.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.blackcompany.eeos.calendar.application.exception;

import com.blackcompany.eeos.common.exception.BusinessException;
import com.blackcompany.eeos.member.application.model.Department;
import org.springframework.http.HttpStatus;

public class DeniedCalendarTypeException extends BusinessException {

private static final String FAIL_CODE = "10001";
private final Department department;

public DeniedCalendarTypeException(Department department) {
super(FAIL_CODE, HttpStatus.BAD_REQUEST);
this.department = department;
}

@Override
public String getMessage() {
return String.format("%s 부서는 해당 타입의 칼렌더를 생성할 수 없습니다.", department.name());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.blackcompany.eeos.calendar.application.exception;

import com.blackcompany.eeos.common.exception.BusinessException;
import org.springframework.http.HttpStatus;

public class DeniedCalendarUpdateException extends BusinessException {

private static final String FAIL_CODE = "10002";

public DeniedCalendarUpdateException() {
super(FAIL_CODE, HttpStatus.UNAUTHORIZED);
}
Comment on lines +10 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

401(UNAUTHORIZED) 대신 403(FORBIDDEN) 사용 권장

인증은 되었으나 권한이 없는 상황이므로 403이 더 적합합니다. 현재 401은 인증 실패 의미로 오해될 수 있습니다.

-        super(FAIL_CODE, HttpStatus.UNAUTHORIZED);
+        super(FAIL_CODE, HttpStatus.FORBIDDEN);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public DeniedCalendarUpdateException() {
super(FAIL_CODE, HttpStatus.UNAUTHORIZED);
}
public DeniedCalendarUpdateException() {
super(FAIL_CODE, HttpStatus.FORBIDDEN);
}
🤖 Prompt for AI Agents
In
eeos/src/main/java/com/blackcompany/eeos/calendar/application/exception/DeniedCalendarUpdateException.java
around lines 10 to 12, the exception currently returns HttpStatus.UNAUTHORIZED
but this scenario represents an authenticated user lacking permission; change
the status to HttpStatus.FORBIDDEN by replacing HttpStatus.UNAUTHORIZED with
HttpStatus.FORBIDDEN in the constructor call (ensure imports remain valid and
adjust any tests or callers expecting 401 to expect 403).


@Override
public String getMessage() {
return "캘린더를 수정할 권한이 없습니다.";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.blackcompany.eeos.calendar.application.exception;

import com.blackcompany.eeos.common.exception.BusinessException;
import org.springframework.http.HttpStatus;

public class InvalidDateException extends BusinessException {

private static final String FAIL_CODE = "10003";

public InvalidDateException() {
super(FAIL_CODE, HttpStatus.BAD_REQUEST);
}

@Override
public String getMessage() {
return "행사 시작 날짜는 행사 종료 날짜보다 이후일 수 없습니다.";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.blackcompany.eeos.calendar.application.exception;

import com.blackcompany.eeos.common.exception.BusinessException;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public class NotFoundCalendarTypeException extends BusinessException {

private static final String FAIL_CODE = "10000";

public NotFoundCalendarTypeException() {
super(FAIL_CODE, HttpStatus.BAD_REQUEST);
}

@Override
public String getMessage() {
return "캘린더 타입이 존재하지 않습니다.";
}
}
Loading