Skip to content

Comments

[박승민] Sprint4#136

Open
raonPsm wants to merge 30 commits intocodeit-bootcamp-spring:박승민from
raonPsm:sprint4
Open

[박승민] Sprint4#136
raonPsm wants to merge 30 commits intocodeit-bootcamp-spring:박승민from
raonPsm:sprint4

Conversation

@raonPsm
Copy link
Collaborator

@raonPsm raonPsm commented Feb 11, 2026

[SB] 스프린트 미션 4

🏔️ 프로젝트 마일스톤

  • 컨트롤러 레이어 추가 및 웹 API 구현
  • Postman 테스트

📝 요구사항

✏️ 기본 요구사항

컨트롤러 레이어 구현

  • DiscodeitApplication의 테스트 로직은 삭제하세요.
  • 지금까지 구현한 서비스 로직을 활용해 웹 API를 구현하세요.
    • 이때 @RequestMapping만 사용해 구현해보세요.
      (웹 API 요구사항)
  • 웹 API의 예외를 전역으로 처리하세요.

API 테스트

  • Postman을 활용해 컨트롤러를 테스트하세요.
    • Postman API 테스트 결과를 다음과 같이 export하여 PR에 첨부해주세요.

웹 API 요구사항

👤 사용자 관리

  • 사용자를 등록할 수 있다.
  • 사용자 정보를 수정할 수 있다.
  • 사용자를 삭제할 수 있다.
  • 모든 사용자를 조회할 수 있다.
  • 사용자의 온라인 상태를 업데이트할 수 있다.

🔑 권한 관리

  • 사용자는 로그인할 수 있다.

💬 채널 관리

  • 공개 채널을 생성할 수 있다.
  • 비공개 채널을 생성할 수 있다.
  • 공개 채널의 정보를 수정할 수 있다.
  • 채널을 삭제할 수 있다.
  • 특정 사용자가 볼 수 있는 모든 채널 목록을 조회할 수 있다.

✉️ 메시지 관리

  • 메시지를 보낼 수 있다.
  • 메시지를 수정할 수 있다.
  • 메시지를 삭제할 수 있다.
  • 특정 채널의 메시지 목록을 조회할 수 있다.

📥 메시지 수신 정보 관리

  • 특정 채널의 메시지 수신 정보를 생성할 수 있다.
  • 특정 채널의 메시지 수신 정보를 수정할 수 있다.
  • 특정 사용자의 메시지 수신 정보를 조회할 수 있다.

📁 바이너리 파일 다운로드

  • 바이너리 파일을 1개 또는 여러 개 조회할 수 있다.

✏️ 심화 요구사항

📂 정적 리소스 서빙

  • 사용자 목록 조회, BinaryContent 파일 조회 API를 다음의 조건을 만족하도록 수정하세요.
    • 사용자 목록 조회
      • url: /api/user/findAll
      • 요청:
        • 파라미터, 바디 없음
      • 응답: ResponseEntity<List<UserDto>>
        public record UserDto(
                UUID id,
                Instant createdAt,
                Instant updatedAt,
                String username,
                String email,
                UUID profileId,
                Boolean online
        ) {
        }
    • BinaryContent 파일 조회
      • url: /api/binaryContent/find
      • 요청
        • 파라미터: binaryContentId
        • 바디: 없음
      • 응답: ResponseEntity<BinaryContent>
  • 다음의 파일을 활용하여 사용자 목록을 보여주는 화면을 서빙해보세요.
    • static-resources.zip
스크린샷 2026-02-10 182837

🤖 생성형 AI 활용

  • 생성형 AI (Claude, ChatGPT 등)를 활용해서 위 이미지와 비슷한 화면을 생성 후 서빙해보세요.

🔄 주요 변경사항

🙇🏽‍♂️ 멘토에게

  • sprint3 코드 리뷰 사항 아직 수정 중입니다. 빠른 시일 내에 수정해서 커밋 올리겠습니다!
  • postman 파일 프로젝트 폴더 /postman에 첨부하였습니다!

@raonPsm raonPsm self-assigned this Feb 11, 2026
@raonPsm raonPsm added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Feb 11, 2026
@joonfluence joonfluence self-requested a review February 14, 2026 15:17
Comment on lines +44 to +53
// 다건 조회 (특정 메시지의 첨부파일 목록 등)
// 사용자가 메시지를 보낼 때 사진 3장을 한 번에 올렸다고 가정해보면
// 프론트에서 사진 3장의 상세 정보를 가져오기 위해 다건 조회
@RequestMapping(value = "/find",method = RequestMethod.GET)
public ResponseEntity<BinaryContent> findBinaryContent(
@RequestParam UUID binaryContentId
) {
BinaryContent content = binaryContentService.findContent(binaryContentId);
return ResponseEntity.ok(content);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

RESTFul API에서 GET이 이미 자원에 대한 조회의 의미를 내포하기 때문에 /find 굳이 추가할 필요 없습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

// GET /api/binaryContents - 여러 첨부 파일 조회
    @GetMapping
    public ResponseEntity<List<BinaryContentResponse>> findAllByIdIn(
            @RequestParam("binaryContentIds") List<UUID> binaryContentIds
    ) {
        List<BinaryContentResponse> binaryContents = binaryContentService.findAllByIdIn(binaryContentIds);
        return ResponseEntity.ok(binaryContents);
    }
  • 위와 같이 수정하였습니다.

Comment on lines +27 to +39
// 공개 채널을 생성할 수 있다.
@RequestMapping(value = "/public", method = RequestMethod.POST)
public ResponseEntity<ChannelResponse> createPublic(@RequestBody PublicChannelCreateRequest request) {
ChannelResponse response = channelService.createPublic(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

// 비공개 채널을 생성할 수 있다.
@RequestMapping(value = "/private", method = RequestMethod.POST)
public ResponseEntity<ChannelResponse> creatPrivate(@RequestBody PrivateChannelCreateRequest request) {
ChannelResponse response = channelService.createPrivate(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

과제에서 요구사항 대로 하신 것 같아 이해하지만, [POST] "/api/channels/public, private" private, public을 Body 값 안에 넣어서 처리하는 방식이 더 깔끔할 것 같긴 합니다. 정말 분리되어야 하면 모르겠지만 거의 구현 코드가 비슷할거거든요

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

public/private 여부를 Body 값으로 처리하는 것이 '리소스에 대한 행위는 HTTP 메서드로, 구체적인 특성은 데이터(Body)로'라는 측면에서 더 Restful한 방식이라는 점에 동의합니다.

다만, 이번 과제에서 제시된 API 명세서를 준수하기 위해 현재 구조를 유지하게 되었습니다.

Comment on lines +7 to +14
public record MessageResponse (
UUID id,
String content,
UUID channelId,
UUID authorId,
List<UUID> attachmentIds,
Instant createAt
) {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

ResponseDTO 별도로 선언해주신 것 좋네요 👍

Comment on lines +112 to +121
private MessageResponse toResponse(Message message) {
return new MessageResponse(
message.getId(),
message.getContent(),
message.getChannelId(),
message.getAuthorId(),
message.getAttachmentIds(),
message.getCreatedAt()
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

ResponseDto로 변환하는 역할이 Service의 책임일까요? 한번 고민해보시면 좋을 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

DTO 변환 로직은 Controller나 별도의 Mapper로 분리하는 방향으로 개선하는 것이 좋은 이유에 대해서 배울 수 있었습니다.
DTO는 보통 클라이언트에 보여줄 데이터 규격인데, Service가 DTO를 직접 만들면, 화면 요구사항이 바뀔 때마다 Service 코드까지 수정해야 된다는 문제를 인식했습니다.
스프린트 미션 5 제출 시 해당 내용 적용하도록 하겠습니다.

Comment on lines +99 to +133
User user = userRepository.findById(userId)
.orElseThrow(() -> new NoSuchElementException("해당 유저를 찾을 수 없습니다. id: " + userId));

// 새 프로필 이미지가 전달되었다면 저장 - 이미지 수정 안했을 경우 기존 id 유지
UUID newProfileImageId = user.getProfileImageId();

if(request.profileImage() != null) {
// 기존에 설정된 프로필 이미지가 있다면 삭제
// TODO: 현재 profileId가 null일 경우 기본 프로필 설정으므로 / 기존 설정된 프로필 이미지가 기본 프로필인지 구분하기 위해
// 기본 프로필 설정을 null이 아니라 명시적으로 따른 사진으로 지정해 줄 필요성 있어보임 -> entity 수정 필요
if(user.getProfileImageId() != null) {
binaryContentRepository.deleteById(user.getProfileImageId());
}

// 새 이미지 생성 및 저장
BinaryContent binaryContent = createBinaryContent(request.profileImage());
binaryContentRepository.save(binaryContent);

// 새 ID로 교체
newProfileImageId = binaryContent.getId();
}

// 유저 정보 수정
user.update(
request.username(),
request.email(),
request.password(),
newProfileImageId
);
User updatedUser = userRepository.save(user);

UserStatus status = userStatusRepository.findByUserId(userId)
.orElseThrow(() -> new IllegalStateException("유저 상태 데이터가 누락되었습니다. id: " + userId));

return toResponse(updatedUser, status);
Copy link
Collaborator

Choose a reason for hiding this comment

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

각각 별도의 메서드로 분리할 수 있을 것 같은데 한번 분리해보시죠~!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

    @Transactional
    @Override
    public User update(UUID userId,
                       UserUpdateRequest userUpdateRequest,
                       Optional<BinaryContentCreateRequest> optionalProfileCreateRequest) {
        // 유저 존재 확인
        User user = findByUserId(userId);

        String newUsername = userUpdateRequest.newUsername();
        String newEmail = userUpdateRequest.newEmail();

        // FIX: 본인의 현재 정보와 다를 때만 중복 검사 실시
        if (!user.getEmail().equals(newEmail) && userRepository.existsByEmail(newEmail)) {
            throw new IllegalArgumentException("이미 사용중인 이메일(email)입니다.: " + newEmail);
        }
        if (!user.getUsername().equals(newUsername) && userRepository.existsByUsername(newUsername)) {
            throw new IllegalArgumentException("이미 존재하는 사용자 이름(username)입니다.: " + newUsername);
        }

        // 회원가입 시 사진을 등록했다면 기본 프로필로 돌아갈 수 있는 기능 없음
        // -> 업데이트 시 파일이 존재하면 기존 사진 삭제 후 새로 저장, 파일이 없으면 기존 사진 유지
        UUID nullableProfileId = resolveNullableProfileId(user, optionalProfileCreateRequest);

        // user 업데이트 및 저장
        user.update(newUsername, newEmail, userUpdateRequest.newPassword(), nullableProfileId);
        return userRepository.save(user);
    }
private UUID resolveNullableProfileId(User user,
                                          Optional<BinaryContentCreateRequest> optionalProfileCreateRequest) {

        return optionalProfileCreateRequest
                .map(profileRequest -> {
                    Optional.ofNullable(user.getProfileId())
                            .ifPresent(binaryContentRepository::deleteById);

                    BinaryContent binaryContent = new BinaryContent(
                            profileRequest.fileName(),
                            profileRequest.contentType(),
                            profileRequest.bytes()
                    );
                    return binaryContentRepository.save(binaryContent).getId();
                })
                .orElse(user.getProfileId());
    }
  • 전반적으로 메서드 수정하였습니다.
  • 일부 별도의 메서드로 분리하였습니다.

Copy link
Collaborator

@joonfluence joonfluence left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants