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
ca816b4
[FEAT] 웹소켓을 활용한 정보 공유 파이프라인 구현 (#221)
coli-geonwoo Nov 25, 2025
563db63
[FEAT] 미완료된 투표 완료 처리 (#227)
unifolio0 Dec 3, 2025
dcba2c9
[FEAT] 기관별 템플릿 조회 기능 구현 (#228)
leegwichan Dec 4, 2025
6e6e123
[CHORE] 무중단 배포 (#226)
unifolio0 Dec 5, 2025
e118bad
[FEAT] 기관별 템플릿 조회 기능 구현 (#229)
leegwichan Dec 11, 2025
3bd25cf
[CHORE] NoSuchMethodError를 openapi 버전이 호환되도록 수정하여 해결 (#231)
coli-geonwoo Dec 17, 2025
4c0045b
[Test] 시간 기반 테스트 수정 (#232)
leegwichan Dec 22, 2025
068748b
[REFACTOR] 인프라 구조를 Docker로 전환 (#237)
leegwichan Feb 18, 2026
7584705
[CHORE] 팀이름 최대 길이를 15자로 수정 (#239)
coli-geonwoo Feb 26, 2026
b531985
[REFACTOR] 인프라 구조를 Docker로 전환 - 2 (#240)
leegwichan Mar 9, 2026
a1662d7
[FEAT] 화면공유 DTO 변경 (#243)
coli-geonwoo Apr 13, 2026
d89c56f
[FEAT] 하트비트 및 인증로직 추가 (#246)
coli-geonwoo May 11, 2026
079dfc8
[FIX] 임시 인프라 러너 태그에 맞게 CD 스크립트 수정 (#248)
coli-geonwoo May 11, 2026
63b42ab
[FIX] 배포 에러 해결 (#250)
coli-geonwoo May 24, 2026
cd84bfd
[FIX] Nginx 설정 정보가 매칭되도록 zero-downtime.sh 파일 수정 (#252)
coli-geonwoo May 25, 2026
fe9716d
[REFACTOR] 사회자 임시 토큰 발급 API 변경 (#254)
coli-geonwoo May 25, 2026
cf2f750
[FEAT] 비회원용 테이블정보 조회 메서드 추가 (#257)
coli-geonwoo Jul 2, 2026
264c96a
[FEAT] 글로벌 템플릿을 위한 기관/기관템플릿에 language 필드 추가 (#259)
coli-geonwoo Jul 29, 2026
1e47ebe
[CHORE] 토론 템플릿 사진 추가 (#261)
coli-geonwoo Jul 31, 2026
cf3b8c6
Merge branch 'main' into develop
coli-geonwoo Sep 1, 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
4 changes: 2 additions & 2 deletions .github/workflows/Dev_CD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

- name: Setting dev-secret.yml
run: |
echo "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml
echo "${{ secrets.DEV_SECRET_TEMP_YML }}" > ./src/main/resources/dev-secret.yml

- name: Set up JDK 17
uses: actions/setup-java@v4
Expand Down Expand Up @@ -52,7 +52,7 @@ jobs:

deploy:
needs: build
runs-on: dev
runs-on: dev-temp

steps:
- name: Download artifact file
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/Prod_CD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

- name: Setting prod-secret.yml
run: |
echo "${{ secrets.PROD_SECRET_YML }}" > ./src/main/resources/prod-secret.yml
echo "${{ secrets.PROD_SECRET_TEMP_YML }}" > ./src/main/resources/prod-secret.yml

- name: Set up JDK 17
uses: actions/setup-java@v4
Expand Down Expand Up @@ -52,7 +52,7 @@ jobs:

deploy:
needs: build
runs-on: prod
runs-on: prod-temp

steps:
- name: Download artifact file
Expand Down
2 changes: 1 addition & 1 deletion scripts/dev/zero-downtime-deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ switch_nginx_upstream() {
log "Switching nginx upstream to port $new_port"
sudo cp "$nginx_conf" "$backup_conf"

sed "s/server 127\.0\.0\.1:[0-9]\+;/server 127.0.0.1:$new_port;/" "$nginx_conf" > "$temp_conf"
sed "s|proxy_pass http://127\.0\.0\.1:[0-9]\+;|proxy_pass http://127.0.0.1:$new_port;|" "$nginx_conf" > "$temp_conf"
sudo cp "$temp_conf" "$nginx_conf"

if ! sudo nginx -t 2>/dev/null; then
Expand Down
2 changes: 1 addition & 1 deletion scripts/nginx-switch-port.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ log "Backing up current nginx configuration"
sudo cp "$NGINX_CONF" "$BACKUP_CONF"

log "Updating nginx upstream to port $NEW_PORT"
sed "s/server 127\.0\.0\.1:[0-9]\+;/server 127.0.0.1:$NEW_PORT;/" "$NGINX_CONF" > "$TEMP_CONF"
sed "s|proxy_pass http://127\.0\.0\.1:[0-9]\+;|proxy_pass http://127.0.0.1:$NEW_PORT;|" "$NGINX_CONF" > "$TEMP_CONF"

log "Configuration changes:"
diff "$NGINX_CONF" "$TEMP_CONF" || true
Expand Down
2 changes: 1 addition & 1 deletion scripts/prod/zero-downtime-deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ switch_nginx_upstream() {
log "Switching nginx upstream to port $new_port"
sudo cp "$nginx_conf" "$backup_conf"

sed "s/server 127\.0\.0\.1:[0-9]\+;/server 127.0.0.1:$new_port;/" "$nginx_conf" > "$temp_conf"
sed "s|proxy_pass http://127\.0\.0\.1:[0-9]\+;|proxy_pass http://127.0.0.1:$new_port;|" "$nginx_conf" > "$temp_conf"
sudo cp "$temp_conf" "$nginx_conf"

if ! sudo nginx -t 2>/dev/null; then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public Object resolveArgument(MethodParameter parameter, Message<?> message) {
throw new DTClientErrorException(ClientErrorCode.UNAUTHORIZED_MEMBER);
}

String email = authManager.resolveAccessToken(token);
String email = authManager.resolveChairmanToken(token);
return authService.getMember(email);
}
}
Expand Down
25 changes: 23 additions & 2 deletions src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.debatetimer.config.sharing;

import com.debatetimer.config.CorsProperties;
import java.time.Duration;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
Expand All @@ -15,6 +18,12 @@
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private static final long SERVER_TO_CLIENT_HEARTBEAT_DURATION = Duration.ofSeconds(10).toMillis();
private static final long CLIENT_TO_SERVER_HEARTBEAT_DURATION = Duration.ofSeconds(10).toMillis();
private static final long SOCKJS_HEART_BEAT_DURATION = Duration.ofSeconds(10).toMillis();
private static final String HEART_BEAT_THREAD_PREFIX = "wss-heartbeat-";
private static final int HEART_BEAT_THREAD_COUNT = 1;

private final CorsProperties corsProperties;
private final WebSocketAuthMemberResolver webSocketAuthMemberResolver;

Expand All @@ -25,14 +34,26 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/room", "/chairman");
registry.enableSimpleBroker("/room", "/chairman")
.setHeartbeatValue(new long[]{SERVER_TO_CLIENT_HEARTBEAT_DURATION, CLIENT_TO_SERVER_HEARTBEAT_DURATION})
.setTaskScheduler(heartBeatScheduler());
registry.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns(corsProperties.getCorsOrigin())
.withSockJS();
.withSockJS()
.setHeartbeatTime(SOCKJS_HEART_BEAT_DURATION);
}

@Bean
public ThreadPoolTaskScheduler heartBeatScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(HEART_BEAT_THREAD_COUNT);
scheduler.setThreadNamePrefix(HEART_BEAT_THREAD_PREFIX);
scheduler.initialize();
return scheduler;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public CustomizeTableResponse getTable(
@PathVariable Long tableId,
@AuthMember Member member
) {
return customizeService.findTable(tableId, member);
return customizeService.findMemberTable(tableId, member);
}

@PutMapping("/api/table/customize/{tableId}")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.debatetimer.controller.organization;

import com.debatetimer.domain.organization.Language;
import com.debatetimer.dto.organization.OrganizationResponses;
import com.debatetimer.service.organization.OrganizationService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
Expand All @@ -14,7 +16,8 @@ public class OrganizationController {
private final OrganizationService organizationService;

@GetMapping("/api/organizations/templates")
public ResponseEntity<OrganizationResponses> getOrganizationTemplates() {
return ResponseEntity.ok(organizationService.findAll());
public ResponseEntity<OrganizationResponses> getOrganizationTemplates(
@RequestParam(defaultValue = "KO_KR") Language language) {
return ResponseEntity.ok(organizationService.findAll(language));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.debatetimer.controller.sharing;

import com.debatetimer.controller.auth.AuthMember;
import com.debatetimer.controller.tool.jwt.AuthManager;
import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.customize.response.CustomizeTableResponse;
import com.debatetimer.dto.sharing.response.ChairmanTokenResponse;
import com.debatetimer.service.customize.CustomizeService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class SharingRestController {

private final CustomizeService customizeService;
private final AuthManager authManager;

@GetMapping("/api/live/{tableId}/chairman-token")
public ChairmanTokenResponse issueChairmanToken(
@AuthMember Member member,
@PathVariable("tableId") long tableId
) {
long debateTime = customizeService.findDebateTime(tableId, member);
String chairmanToken = authManager.issueChairmanToken(member, debateTime * 2);
return new ChairmanTokenResponse(chairmanToken);
}

@GetMapping("/api/live/table/customize/{tableId}")
@ResponseStatus(HttpStatus.OK)
public CustomizeTableResponse getTable(
@PathVariable long tableId
) {
return customizeService.findTable(tableId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@
import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.sharing.request.SharingRequest;
import com.debatetimer.dto.sharing.response.SharingResponse;
import com.debatetimer.service.sharing.SharingService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class SharingController {
@RequiredArgsConstructor
public class SharingWebSocketController {

private final SharingService sharingService;

@MessageMapping("/event/{roomId}")
@SendTo("/room/{roomId}")
public SharingResponse share(
@AuthMember Member member,
@DestinationVariable(value = "roomId") long roomId,
@Payload SharingRequest request
@Valid @Payload SharingRequest request
) {
return new SharingResponse(request.time());
return sharingService.share(request);
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.debatetimer.controller.tool.jwt;

import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.member.JwtTokenResponse;
import com.debatetimer.dto.member.MemberInfo;
import java.time.Duration;
Expand All @@ -20,6 +21,11 @@ public JwtTokenResponse issueToken(MemberInfo memberInfo) {
return new JwtTokenResponse(accessToken, refreshToken, refreshTokenExpiration);
}

public String issueChairmanToken(Member member, long expiration) {
MemberInfo memberInfo = new MemberInfo(member.getEmail());
return jwtTokenProvider.createChairmanToken(memberInfo, expiration);
}

public JwtTokenResponse reissueToken(String refreshToken) {
String email = jwtTokenResolver.resolveRefreshToken(refreshToken);
MemberInfo memberInfo = new MemberInfo(email);
Expand All @@ -37,4 +43,8 @@ public String resolveAccessToken(String accessToken) {
public String resolveRefreshToken(String refreshToken) {
return jwtTokenResolver.resolveRefreshToken(refreshToken);
}

public String resolveChairmanToken(String chairmanToken) {
return jwtTokenResolver.resolveChairmanToken(chairmanToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public String createRefreshToken(MemberInfo memberInfo) {
return createToken(memberInfo, refreshTokenExpiration, TokenType.REFRESH_TOKEN);
}

public String createChairmanToken(MemberInfo memberInfo, long expirationSeconds) {
return createToken(memberInfo, Duration.ofSeconds(expirationSeconds), TokenType.CHAIRMAN_TOKEN);
}

private String createToken(MemberInfo memberInfo, Duration expiration, TokenType tokenType) {
Date now = new Date();
Date expiredDate = new Date(now.getTime() + expiration.toMillis());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public String resolveRefreshToken(String refreshToken) {
return resolveToken(refreshToken, TokenType.REFRESH_TOKEN);
}

public String resolveChairmanToken(String chairmanToken) {
return resolveToken(chairmanToken, TokenType.CHAIRMAN_TOKEN);
}

private String resolveToken(String token, TokenType tokenType) {
try {
Claims claims = Jwts.parserBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public enum TokenType {

ACCESS_TOKEN,
REFRESH_TOKEN
REFRESH_TOKEN,
CHAIRMAN_TOKEN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.debatetimer.domain.organization;

public enum Language {
US_EN,
KO_KR,
;
}
28 changes: 28 additions & 0 deletions src/main/java/com/debatetimer/domain/sharing/TimerEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.debatetimer.domain.sharing;

import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;

@Getter
public class TimerEvent {

@NotNull
private final TimerEventType eventType;

@Nullable
private final TimerEventData timerEventData;

public TimerEvent(
TimerEventType eventType,
@Nullable TimerEventData timerEventData
) {
eventType.validateEventData(timerEventData);
this.eventType = eventType;
this.timerEventData = timerEventData;
}

public TimerEvent(TimerEventType eventType) {
this(eventType, null);
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/debatetimer/domain/sharing/TimerEventData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.debatetimer.domain.sharing;

import com.debatetimer.domain.customize.CustomizeBoxType;
import com.debatetimer.domain.customize.Stance;
import com.debatetimer.exception.custom.DTClientErrorException;
import com.debatetimer.exception.errorcode.ClientErrorCode;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;

@Getter
public class TimerEventData {

@NotNull
private final CustomizeBoxType timerType;

private final int sequence;

@Nullable
private final Stance currentTeam;

private final long remainingTime;

public TimerEventData(
CustomizeBoxType timerType,
int sequence,
@Nullable Stance currentTeam,
long remainingTime
) {
validateCurrentTeam(timerType, currentTeam);
this.timerType = timerType;
this.sequence = sequence;
this.currentTeam = currentTeam;
this.remainingTime = remainingTime;
}

private void validateCurrentTeam(CustomizeBoxType timerType, Stance currentTeam) {
if (timerType.isTimeBased() && currentTeam == null) {
throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BASED_TIMER_EVENT_DATA);
}

if (!timerType.isTimeBased() && currentTeam != null) {
throw new DTClientErrorException(ClientErrorCode.INVALID_NORMAL_TIMER_EVENT_DATA);
}
}
}
Loading
Loading