Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e591168
Merge pull request #170 from TackitOnboarding/dev
yeongsinkeem Jan 13, 2026
67c3595
Merge pull request #171 from TackitOnboarding/dev
yeongsinkeem Jan 13, 2026
a90100d
Merge pull request #172 from TackitOnboarding/dev
yeongsinkeem Jan 14, 2026
b774d65
Merge pull request #173 from TackitOnboarding/dev
yeongsinkeem Jan 14, 2026
e62e0dd
Merge pull request #174 from TackitOnboarding/dev
yeongsinkeem Jan 14, 2026
37a7d6e
Merge pull request #175 from TackitOnboarding/dev
yeongsinkeem Jan 14, 2026
dbbf06f
Merge pull request #177 from TackitOnboarding/dev
yeongsinkeem Jan 15, 2026
004f304
Merge pull request #183 from TackitOnboarding/dev
yeongsinkeem Jan 24, 2026
3e6be51
Merge remote-tracking branch 'origin/dev_temp' into feat/#186-calenda…
tishakong Feb 5, 2026
92f1b5e
Fix: 초기 admin 중복 생성 오류 해결
tishakong Feb 10, 2026
e65880c
feat: #186- Event, EventParticipant, EventScope 모델링
tishakong Feb 10, 2026
7955020
feat: #186-일정 관련 권한 확인 메서드, 일정 참가자 생성 메서드 구현
tishakong Feb 10, 2026
37879c7
feat: #186-일정 생성 API 구현
tishakong Feb 10, 2026
fa24154
feat: #186-일정 수정 API 구현
tishakong Feb 10, 2026
4d92ff7
feat: #186-특정 조직의 월간 일정 조회
tishakong Feb 10, 2026
51a6e9a
feat: #186-특정 조직의 다가오는 일정 조회
tishakong Feb 10, 2026
2d2bc1a
Fix: 일정 생성/수정 시 필요한 EventParticipantDto 추가
tishakong Feb 10, 2026
21eca20
Fix: 일정 관련 권한 확인 메서드에 필요한 JPA 추가
tishakong Feb 10, 2026
99592ab
feat: #186-특정 조직의 일정 삭제 API 구현
tishakong Feb 10, 2026
66bfb0e
feat: #186-특정 조직의 일정 상세 조회 API 구현
tishakong Feb 10, 2026
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
Expand Up @@ -24,7 +24,7 @@ public class CommonDataInitializer implements CommandLineRunner {
// args를 꼭 배열로 넘겨야 하는 건 아니고 가변적으로 받을 수 있다는 의도를 나타냄
@Override
public void run(String... args) throws Exception {
if (memberRepository.findByEmail("admin").isEmpty()) {
if (memberRepository.findByEmail("contact.tackit@gmail.com").isEmpty()) {
Member admin = Member.builder()
.email("contact.tackit@gmail.com")
.password(passwordEncoder.encode("admin1")) // BCrypt 인코딩
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.example.tackit.domain.auth.login.repository;

import org.example.tackit.domain.entity.Member;
import org.example.tackit.domain.entity.Org.MemberOrg;
import org.example.tackit.domain.entity.Org.OrgStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand Down Expand Up @@ -41,4 +43,10 @@ public interface MemberOrgRepository extends JpaRepository<MemberOrg, Long> {

// 조직 ID와 멤버 ID로 가입 여부 확인 (중복 가입 방지용)
boolean existsByMemberIdAndOrganizationId(Long memberId, Long orgId);

Optional<MemberOrg> findByMemberIdAndOrganizationId(Long memberId, Long orgId);

boolean existsByMemberIdAndOrganizationIdAndOrgStatus(Long memberId, Long orgId, OrgStatus status);

Optional<MemberOrg> findByMemberIdAndOrganizationIdAndOrgStatus(Long memberId, Long orgId, OrgStatus status);
}
92 changes: 92 additions & 0 deletions src/main/java/org/example/tackit/domain/entity/Event.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package org.example.tackit.domain.entity;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.example.tackit.domain.entity.Org.Organization;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

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

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "event")
public class Event{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "event_id")
private Long id;

@Column(nullable = false)
private String title;

@Column(nullable = false)
private LocalDateTime startsAt;

@Column(nullable = false)
private LocalDateTime endsAt;

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;

@Column(columnDefinition = "TEXT")
private String description;

@Column(name = "color_chip", nullable = false)
private String colorChip;

@Enumerated(EnumType.STRING)
@Column(name = "event_scope", nullable = false)
private EventScope eventScope;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "org_id", nullable = false)
private Organization organization;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member creator;

@OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true)
private List<EventParticipant> participants = new ArrayList<>();

@Builder
public Event(String title, LocalDateTime startsAt, LocalDateTime endsAt, LocalDateTime createdAt, LocalDateTime updatedAt,
String description, String colorChip, EventScope eventScope,
Organization organization, Member creator) {
this.title = title;
this.startsAt = startsAt;
this.endsAt = endsAt;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.description = description;
this.colorChip = colorChip;
this.eventScope = eventScope;
this.organization = organization;
this.creator = creator;
}

public void update(String title, LocalDateTime startsAt, LocalDateTime endsAt,
String description, String colorChip, EventScope eventScope) {
if (title != null) this.title = title;
if (startsAt != null) this.startsAt = startsAt;
if (endsAt != null) this.endsAt = endsAt;
if (description != null) this.description = description;
if (colorChip != null) this.colorChip = colorChip;
if (eventScope != null) this.eventScope = eventScope;
}

// 참여자 목록 초기화 (참여자 수정 시 사용)
public void clearParticipants() {
this.participants.clear();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.example.tackit.domain.entity;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.example.tackit.domain.entity.Org.MemberOrg;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@Table(name = "event_participant")
public class EventParticipant {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "event_participant_id")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "event_id", nullable = false)
private Event event;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_org_id", nullable = false)
private MemberOrg memberOrg;

@CreatedDate
private LocalDateTime createdAt;

@Builder
public EventParticipant(Event event, MemberOrg memberOrg) {
this.event = event;
this.memberOrg = memberOrg;
}

// 연관관계 편의 메서드
public void assignEvent(Event event) {
this.event = event;
event.getParticipants().add(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.example.tackit.domain.entity;

public enum EventScope {
PARTIAL,
ALL
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package org.example.tackit.domain.event.controller;

import lombok.RequiredArgsConstructor;
import org.example.tackit.domain.auth.login.security.CustomUserDetails;
import org.example.tackit.domain.event.dto.EventCreateReqDto;
import org.example.tackit.domain.event.dto.EventDetailResDto;
import org.example.tackit.domain.event.dto.EventSimpleResDto;
import org.example.tackit.domain.event.dto.EventUpdateReqDto;
import org.example.tackit.domain.event.service.EventService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/events")
public class EventController {

private final EventService eventService;

/**
* 일정 생성
*/
@PostMapping
public ResponseEntity<?> createEvent(
@RequestBody EventCreateReqDto reqDto,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
// TODO 인증 정보가 없습니다 코드 공통 로직 처리
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다.");
}

Long eventId = eventService.createEvent(reqDto, userDetails.getId());

// TODO ResponseEntity 커스텀 공통 양식 추가
return ResponseEntity.status(HttpStatus.CREATED).body(eventId);
}
Comment on lines +28 to +41

Choose a reason for hiding this comment

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

medium

모든 컨트롤러 메소드에서 userDetails == null 체크가 반복되고 있습니다. Spring Security의 @PreAuthorize("isAuthenticated()") 어노테이션을 사용하거나, 인터셉터 또는 필터를 활용하여 인증 여부를 공통으로 처리하면 코드 중복을 줄이고 컨트롤러 로직을 더 깔끔하게 유지할 수 있습니다. 예를 들어, 클래스 레벨에 @PreAuthorize("isAuthenticated()")를 추가하고 SecurityConfig에서 활성화할 수 있습니다.


/**
* 일정 수정
*/
@PatchMapping("/{eventId}")
public ResponseEntity<?> updateEvent(
@PathVariable Long eventId,
@RequestBody EventUpdateReqDto reqDto,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다.");
}

eventService.updateEvent(eventId, reqDto, userDetails.getId());
return ResponseEntity.status(HttpStatus.OK).body(eventId);
}

/**
* 월간 일정 조회
*/
@GetMapping("/monthly")
public ResponseEntity<?> getMonthlyEvents(
@RequestParam Long orgId,
@RequestParam int year,
@RequestParam int month,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다.");
}

List<EventSimpleResDto> events = eventService.getMonthlyEvents(orgId, year, month, userDetails.getId());
return ResponseEntity.status(HttpStatus.OK).body(events);
}

/**
* 다가오는 일정(사이드바) 조회
*/
@GetMapping("/upcoming")
public ResponseEntity<?> getUpcomingEvents(
@RequestParam Long orgId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다.");
}

List<EventSimpleResDto> events = eventService.getUpcomingEvents(orgId, userDetails.getId());
return ResponseEntity.status(HttpStatus.OK).body(events);
}

/**
* 일정 상세 조회
*/
@GetMapping("/{eventId:[0-9]+}")
public ResponseEntity<?> getEventDetail(
@PathVariable Long eventId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다.");
}

EventDetailResDto detail = eventService.getEventDetail(eventId, userDetails.getId());
return ResponseEntity.status(HttpStatus.OK).body(detail);
}

/**
* 일정 삭제
*/
@DeleteMapping("/{eventId}")
public ResponseEntity<?> deleteEvent(
@PathVariable Long eventId,
@AuthenticationPrincipal CustomUserDetails userDetails
) {
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("인증 정보가 없습니다.");
}

eventService.deleteEvent(eventId, userDetails.getId());
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.example.tackit.domain.event.dto;

import jakarta.validation.constraints.*;
import lombok.*;
import org.example.tackit.domain.entity.EventScope;
import org.example.tackit.domain.entity.Org.OrgType;

import java.time.LocalDateTime;
import java.util.List;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class EventCreateReqDto {

@NotNull(message = "조직 ID는 필수입니다.")
private Long orgId;

@NotBlank(message = "일정 제목은 필수입니다.")
private String title;

@NotNull(message = "시작 시간은 필수입니다.")
private LocalDateTime startsAt;

@NotNull(message = "종료 시간은 필수입니다.")
private LocalDateTime endsAt;

private String description;

@NotNull(message = "참여자 범위(eventScope)는 필수입니다.")
private EventScope eventScope;

@NotNull(message = "참여자 목록 필드는 필수입니다.")
private List<Long> participants;

@NotBlank(message = "색상 코드는 필수입니다.")
private String colorChip;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.example.tackit.domain.event.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;

import java.time.LocalDateTime;
import java.util.List;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EventDetailResDto {

private Long eventId;
private String title;
private LocalDateTime startsAt;
private LocalDateTime endsAt;
private String description;
private String colorChip;
private List<EventParticipantDto> participants;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.example.tackit.domain.event.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EventParticipantDto {
private Long orgMemberId;
private String profileImageUrl;
private String nickname;
}
Loading