Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5a8a0fb
feat: 모니터링과 서버 환경 분리
sunghyuki Nov 19, 2024
93b8fc4
feat: Promtail이 도커 컨테이너의 로그를 읽을 수 있도록 로그 경로의 폴더를 read-only로 설정
sunghyuki Nov 19, 2024
297b25a
feat: Promtail을 spring boot 서버와 동일 인스턴스에 배포되도록 설정
sunghyuki Nov 19, 2024
9bd15e4
fix: promtail port setting
sunghyuki Nov 20, 2024
9bcad84
feat: 스프링 로그 관리를 위한 시스템 구축
sunghyuki Nov 20, 2024
8254e54
feat: 호스트 volume을 spring server, promtail 컨테이너가 공유하도록 설정
sunghyuki Nov 20, 2024
5700da0
feat: Spring Boot Actuator stdout 로그 기록하지 않도록 하는 설정
sunghyuki Nov 20, 2024
edd5560
[#119] 스키마 수정 (#120)
Bellroute Jan 17, 2025
2a2cb4e
feat: Loki에서 로그 정보가 유의미한 정보로 표현되도록 개선
sunghyuki Jan 19, 2025
ed3bd7a
refactor: response log에서 body 제거되도록 수정
sunghyuki Feb 18, 2025
daf553e
fix: discord webhook url encrypt (#125)
Bellroute Mar 3, 2025
4b42575
[#126] 컨텐츠(기록) 신고 기능 관련 API 구현 (#128)
Bellroute Mar 17, 2025
1b52918
[#129] 기록 목록 조회 api에 차단(block) 필터링 추가 (#130)
Bellroute Apr 13, 2025
747db46
fix: discord webhook url encrypt (#132)
Bellroute Apr 16, 2025
ebf9e92
fix: accessToken 만료시간 임시 변경 (#133)
Bellroute Apr 18, 2025
b2f482c
add: 401, 403 핸들러에서 response 객체에 cors 관련 헤더 추가하도록 로직 변경 (#136)
Bellroute May 25, 2025
1c0ba0f
[#138] Record(기록) 테이블에 등반날짜(climbDate) 추가 및 api 수정 (#139)
Bellroute Jul 1, 2025
c282d3c
Feat/137 applying swagger api (#140)
abovenormal Aug 10, 2025
bd8bf76
[#141] 찍볼 crd api 구현 (#143)
Bellroute Aug 26, 2025
41a5a8d
feat: 인프라 마이그레이션으로 인한 datasource url 변경 (#149)
Bellroute Sep 2, 2025
bf3454f
Feature/142 search armspan cp (#147)
abovenormal Oct 28, 2025
2162186
[#150] 찍볼 목록 조회 api 구현 (#151)
Bellroute Dec 2, 2025
a0a5e83
modify: datasource url 변경 (#156)
Bellroute Jan 26, 2026
a39add5
add: 샌박용 컴포즈 파일 추가 (#157)
Bellroute Feb 1, 2026
8aec858
Merge branch 'main' into dev
Bellroute Feb 1, 2026
6558032
add: readme 수정 (#159)
Bellroute Feb 1, 2026
f718d5a
Merge branch 'main' into dev
Bellroute Feb 1, 2026
3b05b69
dev 머지시 cd 테스트를 위한 pr (#161)
Bellroute Feb 1, 2026
d80548f
Merge branch 'main' into dev
Bellroute Feb 1, 2026
6f72c0d
add: readme 수정 (#162)
Bellroute Feb 1, 2026
9381223
fix: github action flow 수정
Bellroute Feb 1, 2026
15a87ee
chore: cicd 테스트를 위한 무의미한 커밋 (#164)
Bellroute Feb 1, 2026
7fb7a5d
Merge branch 'main' into dev
Bellroute Feb 8, 2026
3678b40
chore: docker-compose 네이밍 변경
Bellroute Feb 8, 2026
a98b528
chore: docker-compose dev용 이미지명 변경
Bellroute Feb 8, 2026
e26a372
Merge branch 'main' into dev
Bellroute Feb 8, 2026
a6eb1ca
Merge branch 'main' into dev
Bellroute Feb 8, 2026
498f60d
chore: docker-compose dev용 이미지명 변경
Bellroute Feb 8, 2026
8083056
chore: docker-compose
Bellroute Feb 8, 2026
9056562
chore: docker-compose
Bellroute Feb 8, 2026
30fe001
chore: docker-compose
Bellroute Feb 8, 2026
464405b
chore: docker-compose
Bellroute Feb 8, 2026
ac36053
chore: docker-compose
Bellroute Feb 8, 2026
963a688
chore: docker-compose
Bellroute Feb 8, 2026
6787c86
chore: docker-compose
Bellroute Feb 8, 2026
54a9436
feat: api 작업
abovenormal Feb 11, 2026
6dc7be4
feat: 내정보조회, 피지컬인포 테스트 코드 작성
abovenormal Feb 11, 2026
6a1becf
Merge branch 'dev' into feature/154/physicalInfo_memberInfo_api
abovenormal Feb 11, 2026
55cf6d9
refactor:
abovenormal Mar 18, 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
26 changes: 17 additions & 9 deletions docker-compose-sandbox.yml → docker-compose-dev.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,42 @@
version: '3.8'

name: climingo-dev

services:
climingo-api:
image: climingo/climingo:${TAG} # Spring Boot 애플리케이션 Docker 이미지
container_name: climingo-api
image: climingo/climingo-dev:${TAG} # Spring Boot 애플리케이션 Docker 이미지
container_name: climingo-dev-api
environment:
- JASYPT_PASSWORD=${JASYPT_PASSWORD}
- VERSION=${TAG}
- BUILDTIME=${BUILDTIME}
- LOGGING_FILE_PATH=/logs # 로그 파일 경로를 환경 변수로 설정
- LOGGING_FILE_PATH=/logs/dev # 로그 파일 경로를 환경 변수로 설정
volumes:
- ./logs:/logs # 호스트와 컨테이너 간 로그 파일 공유
- ./logs:/logs/dev # 호스트와 컨테이너 간 로그 파일 공유
ports:
- "8080:8080"
- "8081:8080"
logging: # 로그 드라이버 설정 (json-file 기본값 사용)
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- climingo-network

promtail:
image: grafana/promtail:2.9.0 # Promtail Docker 이미지
container_name: promtail
container_name: promtail-dev
ports:
- "9080:9080"
- "9081:9080"
depends_on:
- climingo-api # Spring Boot 애플리케이션이 먼저 실행되도록 설정
volumes:
- ./logs:/logs # 호스트 로그 디렉토리를 Promtail에 공유
- ./logs:/logs/dev # 호스트 로그 디렉토리를 Promtail에 공유
- ./promtail-config.yml:/etc/promtail/promtail.yml:ro # Promtail 설정 파일
command:
- -config.file=/etc/promtail/promtail.yml # 설정 파일 위치
restart: always
restart: always

networks:
climingo-network:
external: true
10 changes: 7 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
version: '3.8'

name: climingo-prod

services:
climingo-api:
image: climingo/climingo:${TAG} # Spring Boot 애플리케이션 Docker 이미지
Expand All @@ -8,16 +10,18 @@ services:
- JASYPT_PASSWORD=${JASYPT_PASSWORD}
- VERSION=${TAG}
- BUILDTIME=${BUILDTIME}
- LOGGING_FILE_PATH=/logs # 로그 파일 경로를 환경 변수로 설정
- LOGGING_FILE_PATH=/logs/prod # 로그 파일 경로를 환경 변수로 설정
volumes:
- ./logs:/logs # 호스트와 컨테이너 간 로그 파일 공유
- ./logs:/logs/prod # 호스트와 컨테이너 간 로그 파일 공유
ports:
- "8080:8080"
logging: # 로그 드라이버 설정 (json-file 기본값 사용)
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- climingo-network

promtail:
image: grafana/promtail:2.9.0 # Promtail Docker 이미지
Expand All @@ -27,7 +31,7 @@ services:
depends_on:
- climingo-api # Spring Boot 애플리케이션이 먼저 실행되도록 설정
volumes:
- ./logs:/logs # 호스트 로그 디렉토리를 Promtail에 공유
- ./logs:/logs/prod # 호스트 로그 디렉토리를 Promtail에 공유
- ./promtail-config.yml:/etc/promtail/promtail.yml:ro # Promtail 설정 파일
command:
- -config.file=/etc/promtail/promtail.yml # 설정 파일 위치
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import com.climingo.climingoApi.global.auth.RequestMember;
import com.climingo.climingoApi.member.api.request.UpdateNicknameRequest;
import com.climingo.climingoApi.member.api.request.UpdatePhysicalinfo;
import com.climingo.climingoApi.member.api.response.MemberInfoResponse;
import com.climingo.climingoApi.member.application.MemberService;
import com.climingo.climingoApi.member.domain.Member;
import com.climingo.climingoApi.member.domain.PhysicalInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand Down Expand Up @@ -40,6 +42,14 @@ public ResponseEntity<MemberInfoResponse> findMemberInfo(@PathVariable(value = "
return ResponseEntity.ok().body(memberInfoResponse);
}

@PatchMapping("/member/{memberId}/physicalInfo")
public ResponseEntity<Void> updatePhysicalInfo(@RequestMember Member member, @PathVariable(value = "memberId") Long memberId,
@RequestBody @Valid UpdatePhysicalinfo request){
memberService.updatePhysicalInfo(member,memberId,request.getPhysicalInfo());
return ResponseEntity.ok().build();

}

@PatchMapping("/members/{memberId}/nickname")
@Operation(summary = "멤버 닉네임 변경", description = "회원정보 닉네임 변경")
@Parameter(name = "nickname", description = "변경할 닉네임")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.climingo.climingoApi.member.api.request;

import com.climingo.climingoApi.member.domain.PhysicalInfo;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
@JsonDeserialize(builder = UpdatePhysicalinfo.UpdatePhysicalinfoBuilder.class)
public class UpdatePhysicalinfo {

@NotNull(message = "physicalInfo는 필수입니다")
@Valid
@JsonProperty("physicalInfo")
private final PhysicalInfo physicalInfo;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.climingo.climingoApi.member.api.response;

import com.climingo.climingoApi.member.domain.Member;
import com.climingo.climingoApi.member.domain.PhysicalInfo;
import lombok.Getter;

@Getter
Expand All @@ -11,13 +12,15 @@ public class MemberInfoResponse {
private String email;
private String profileUrl;
private String providerType;
private PhysicalInfo physicalInfo;

public MemberInfoResponse(Member member) {
this.memberId = member.getId();
this.nickname = member.getNickname();
this.email = member.getEmail();
this.profileUrl = member.getProfileUrl();
this.providerType = member.getProviderType();
this.physicalInfo = member.getPhysicalInfo();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import com.climingo.climingoApi.member.api.response.MemberInfoResponse;
import com.climingo.climingoApi.member.domain.Member;
import com.climingo.climingoApi.member.domain.PhysicalInfo;
import org.springframework.transaction.annotation.Transactional;

public interface MemberService {

MemberInfoResponse findMemberInfo(Long memberId);

void updateNickname(Member member, Long memberId, String nickname);

void updatePhysicalInfo(Member member, Long memberId, PhysicalInfo physicalInfo);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.climingo.climingoApi.member.api.response.MemberInfoResponse;
import com.climingo.climingoApi.member.domain.Member;
import com.climingo.climingoApi.member.domain.MemberRepository;
import com.climingo.climingoApi.member.domain.PhysicalInfo;
import jakarta.persistence.EntityNotFoundException;
import java.util.NoSuchElementException;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -93,4 +94,13 @@ private boolean isDuplicated(String nickname) {
return memberRepository.existsByNickname(nickname);
}


@Override
@Transactional
public void updatePhysicalInfo(Member member, Long memberId, PhysicalInfo physicalInfo){

member.updatePhysicalinfo(physicalInfo);
memberRepository.save(member);
}

}
14 changes: 14 additions & 0 deletions src/main/java/com/climingo/climingoApi/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,20 @@ public void updateNickname(String nickname) {
this.nickname = nickname;
}

// 소수점 관련 처리 로직
public boolean isPhysicalinfoDouble(PhysicalInfo physicalInfo){

return true;
}

public void updatePhysicalinfo(PhysicalInfo physicalInfo){
if (this.physicalInfo == null) {
this.physicalInfo = physicalInfo;
} else {
this.physicalInfo = this.physicalInfo.merge(physicalInfo);
}
}

public boolean isSameMember(Member member) {
return this.isSameMember(member.getId());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ public class PhysicalInfo {

@JsonProperty("armSpan")
private Double armSpan;

public PhysicalInfo merge(PhysicalInfo update) {
return new PhysicalInfo(
update.height != null ? update.height : this.height,
update.weight != null ? update.weight : this.weight,
update.armSpan != null ? update.armSpan : this.armSpan
);
}
}
4 changes: 2 additions & 2 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ oauth2:
path: ENC(UQVIwN53EBnE0TcwgTT79HYM2KUrEcRDhlx72+W+anbO87WiWJbCSJQKOPqGBNGaze2oi9obarWiRuzW5UdJtLUzNe7icfKNPepJrHZXhVSD5UUO5eHsvbFVeWhVScgeIPxvtbdsk0LMsLH7EbzSuC852HHy0/KQ6j8gJREw/RkyFuHFYJR2VupYASz9jpdabzwA6q9Lh9v7BPqLB2nFzub5xmHMU7HWICvzSHbnTsgXqyQXr7XtvHxQVZN5z4lEAKbk0BB53vm3ammzkxhTo656ZFAgK69n)

ffmpeg:
ffmpeg-path: /usr/local/bin/ffmpeg
ffprobe-path: /usr/local/bin/ffprobe
ffmpeg-path: /opt/homebrew/bin/ffmpeg
ffprobe-path: /opt/homebrew/bin/ffprobe

app:
version: local_temp_version
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.climingo.climingoApi.member.api;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.climingo.climingoApi.global.auth.RequestMember;
import com.climingo.climingoApi.global.exception.GlobalExceptionHandler;
import com.climingo.climingoApi.member.api.response.MemberInfoResponse;
import com.climingo.climingoApi.member.application.MemberService;
import com.climingo.climingoApi.member.domain.Member;
import com.climingo.climingoApi.member.domain.PhysicalInfo;
import com.climingo.climingoApi.member.domain.UserRole;
import com.climingo.climingoApi.message.error.ErrorAlertMessageProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.EntityNotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@DisplayName("MemberController 단위 테스트")
class MemberControllerTest {

private MockMvc mockMvc;
private MemberService memberService;
private ObjectMapper objectMapper;
private Member loginMember;

@BeforeEach
void setUp() {
memberService = mock(MemberService.class);
objectMapper = new ObjectMapper();

loginMember = Member.builder()
.id(1L)
.authId("auth123")
.providerType("kakao")
.nickname("testUser")
.email("test@test.com")
.profileUrl("http://profile.url")
.physicalInfo(new PhysicalInfo(175.5, 70.0, 180.0))
.role(UserRole.USER)
.build();

HandlerMethodArgumentResolver requestMemberResolver = new HandlerMethodArgumentResolver() {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(RequestMember.class) != null
&& Member.class.equals(parameter.getParameterType());
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
return loginMember;
}
};

mockMvc = MockMvcBuilders.standaloneSetup(new MemberController(memberService))
.setCustomArgumentResolvers(requestMemberResolver)
.setControllerAdvice(new GlobalExceptionHandler(mock(ErrorAlertMessageProvider.class)))
.build();
}

@Test
@DisplayName("GET /members - 로그인한 회원이 자신의 정보를 정상 조회한다")
void findMyInfo_success() throws Exception {
MemberInfoResponse response = new MemberInfoResponse(loginMember);
when(memberService.findMemberInfo(eq(1L))).thenReturn(response);

mockMvc.perform(get("/members"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.memberId").value(1L))
.andExpect(jsonPath("$.nickname").value("testUser"))
.andExpect(jsonPath("$.email").value("test@test.com"))
.andExpect(jsonPath("$.profileUrl").value("http://profile.url"))
.andExpect(jsonPath("$.providerType").value("kakao"))
.andExpect(jsonPath("$.physicalInfo.height").value(175.5))
.andExpect(jsonPath("$.physicalInfo.weight").value(70.0))
.andExpect(jsonPath("$.physicalInfo.armSpan").value(180.0));
}

@Test
@DisplayName("GET /members - 존재하지 않는 회원 조회 시 EntityNotFoundException이 발생한다")
void findMyInfo_notFound() throws Exception {
when(memberService.findMemberInfo(eq(1L)))
.thenThrow(new EntityNotFoundException("id가 1인 회원은 존재하지 않습니다"));

mockMvc.perform(get("/members"))
.andExpect(status().isNotFound());
}

@Test
@DisplayName("PATCH /member/{memberId} - 유효한 요청으로 신체 정보를 업데이트한다")
void updatePhysicalInfo_success() throws Exception {
doNothing().when(memberService).updatePhysicalInfo(any(Member.class), eq(1L), any(PhysicalInfo.class));

String requestBody = objectMapper.writeValueAsString(
java.util.Map.of("physicalInfo", java.util.Map.of(
"height", 180.0,
"weight", 75.5,
"armSpan", 185.0
))
);

mockMvc.perform(patch("/member/{memberId}", 1L)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isOk());
}

@Test
@DisplayName("PATCH /member/{memberId} - physicalInfo 없이 호출 시 400 에러가 발생한다")
void updatePhysicalInfo_missingBody() throws Exception {
mockMvc.perform(patch("/member/{memberId}", 1L)
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isBadRequest());
}
}