Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
af7ecee
feat: Add problemId on GetSolutionResponse (#390)
hwangjokim Aug 24, 2025
b306633
devops: Assign reviewer automatically (#392)
hwangjokim Aug 24, 2025
b2196c7
refactor : 백준 API 호출 시 timeout 에러 처리 추가 (#395)
junggyo1020 Aug 25, 2025
337ba59
feat : Require bjNickname field when signing up (#397)
hwangjokim Sep 7, 2025
bdfce34
devops : enable manual deployment trigger (#396)
hwangjokim Sep 7, 2025
ae5d9ba
feat : 반례 게시판 CRUD + 좋아요 기능 (#389)
sh0723 Sep 7, 2025
8880f49
feat:스터디 그룹 검색 API (#391)
s-hwan Sep 16, 2025
9a0b68c
refactor : 알림 카테고리화 (#394)
sh0723 Sep 21, 2025
e5b23b9
refactor : 내 풀이리스트 부분 조회 (#393)
sh0723 Sep 21, 2025
e0b2c16
refactor : getProblems (#398)
sh0723 Sep 21, 2025
950cd49
fix: SQL을 잘못 쓴 이슈,, (#400)
s-hwan Sep 21, 2025
27de1b4
refactor : 반례게시판 (#402)
sh0723 Oct 12, 2025
603601c
feat : 가입 요청 생성 및 조회 승인 및 거절 (#399)
s-hwan Oct 26, 2025
111e844
feat : 내 피드 (#401)
sh0723 Oct 26, 2025
6d8abe3
feat : 홈화면 추천 스터디 (#403)
junggyo1020 Oct 26, 2025
5c7fd81
feat : 반례게시판 이슈 해결 및 정렬을 위한 분기 param 추가 (#404)
junggyo1020 Oct 26, 2025
3674790
[Feat] 문제 수정 API 시, 시작 날짜 검증로직 삭제 (#405)
junggyo1020 Nov 2, 2025
e10e5a8
refactor : 그룹 가입 요청 목록 조회 (#407)
s-hwan Nov 9, 2025
76a6c05
Refactor : 반례리스트 조회 (#406)
sh0723 Nov 16, 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
14 changes: 14 additions & 0 deletions .github/workflows/assign-reviewer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Review Assign

on:
pull_request:
types: [ opened, ready_for_review ]

jobs:
assign:
runs-on: ubuntu-latest
steps:
- uses: hkusu/review-assign-action@v1
with:
assignees: ${{ github.actor }} # assign pull request author
reviewers: s-hwan , rladmstn, sh0723, hwangjokim, junggyo1020 # if draft, assigned when draft is released
11 changes: 5 additions & 6 deletions .github/workflows/prod-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ on:
push:
branches:
- deploy
workflow_dispatch:

permissions:
contents: read


jobs:

build:
runs-on: ubuntu-latest

Expand All @@ -19,13 +18,13 @@ jobs:
- name: JDK 21 Setup
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
java-version: "21"
distribution: "temurin"

- name: CREDENTIAL_NAME, CREDENTIAL_PW
run: |
echo "CREDENTIAL_NAME=${{ secrets.CREDENTIAL_NAME }}" >> $GITHUB_ENV
echo "CREDENTIAL_PW=${{ secrets.CREDENTIAL_PW }}" >> $GITHUB_ENV
echo "CREDENTIAL_PW=${{ secrets.CREDENTIAL_PW }}" >> $GITHUB_ENV

- name: Grant execute permission for gradlew
run: chmod +x gradlew
Expand Down Expand Up @@ -58,4 +57,4 @@ jobs:
chmod 400 private_key.pem
sudo scp -i private_key.pem -o StrictHostKeyChecking=no build/libs/algohub-0.0.1-SNAPSHOT.jar ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/home/${{ secrets.EC2_USERNAME }}/algohub.jar
sudo ssh -i private_key.pem -o StrictHostKeyChecking=no ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} "pgrep java | xargs kill -9; nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=prod /home/${{ secrets.EC2_USERNAME }}/algohub.jar > app.log 2>&1 &"
rm -f private_key.pem
rm -f private_key.pem
9 changes: 5 additions & 4 deletions .github/workflows/rc-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- develop
workflow_dispatch:

jobs:
rc-deploy:
Expand All @@ -16,8 +17,8 @@ jobs:
- name: JDK 21 Setup
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
java-version: "21"
distribution: "temurin"

- name: Setup environment variable from WebDAV
run: |
Expand All @@ -28,7 +29,7 @@ jobs:
- name: CREDENTIAL_NAME, CREDENTIAL_PW
run: |
echo "CREDENTIAL_NAME=${{ secrets.CREDENTIAL_NAME }}" >> $GITHUB_ENV
echo "CREDENTIAL_PW=${{ secrets.CREDENTIAL_PW }}" >> $GITHUB_ENV
echo "CREDENTIAL_PW=${{ secrets.CREDENTIAL_PW }}" >> $GITHUB_ENV

- name: Grant execute permission for gradlew
run: chmod +x gradlew
Expand All @@ -51,4 +52,4 @@ jobs:

- name: Push backend docker image
run: |
docker push docker.hwangdo.kr/algohub-server-rc:latest
docker push docker.hwangdo.kr/algohub-server-rc:latest
1 change: 0 additions & 1 deletion prepare-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ resources_path="./src/main/resources"
is_empty_or_null() {
[ -z "$1" ] || [ "$1" = "null" ]
}

if is_empty_or_null "$CREDENTIAL_NAME" || is_empty_or_null "$CREDENTIAL_PW"; then
echo "필요한 환경변수(CREDENTIAL_NAME, CREDENTIAL_PW) 중 하나 이상이 설정되어 있지 않거나 null입니다."
exit 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
@Retention(RetentionPolicy.RUNTIME)
@Hidden
public @interface AuthedUser {
boolean required() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,19 @@ public boolean supportsParameter(MethodParameter parameter) {
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
AuthedUser authedUser = parameter.getParameterAnnotation(AuthedUser.class);
boolean required = authedUser.required();

String jwt = webRequest.getHeader("Authorization");
if (jwt != null)
return userRepository.findByEmail(tokenProvider.getUserEmail(jwt))
.orElseThrow(() -> new UserValidationException("없는 사용자 입니다."));
else
throw new UserValidationException("로그인 되지 않았습니다.");

if (jwt == null || jwt.isBlank()) {
if (required) {
throw new UserValidationException("로그인되지 않았습니다.");
} else {
return null;
}
}
return userRepository.findByEmail(tokenProvider.getUserEmail(jwt))
.orElseThrow(() -> new UserValidationException("없는 사용자입니다."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
public final class ApiConstants {
public static final String SOLVED_AC_PROBLEM_API_URL = "https://solved.ac/api/v3/problem/lookup?problemIds=";
public static final String BOJ_USER_PROFILE_URL = "https://www.acmicpc.net/user/";
public static final String BOJ_PROBLEM_URL = "www.acmicpc.net";
public static final String BOJ_URL = "www.acmicpc.net";
public static final String SERVER_HTTPS_ENDPOINT = "https://api.algohub.kr";
public static final String RC_SERVER_HTTPS_ENDPOINT = "https://api.rc.algohub.kr";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class BOJResultConstants {
public static final String OVER_OUTPUT_LIMIT = "출력 초과";
public static final String WRONG_OUTPUT_FORMAT = "출력 형식이 잘못되었습니다";
public static final String WRONG_OUTPUT_FORMAT_CUSTOM = "출력 에러";
public static final String SCORE_UNIT = "점";

private BOJResultConstants() {
throw new RuntimeException("Can not instantiate : BOJResultConstants");
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/gamzabat/algohub/enums/JoinRequestStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.gamzabat.algohub.enums;

public enum JoinRequestStatus {
PENDING("pending"),
APPROVE("approve"),
CANCEL("cancel"),
REJECT("reject");
private String value;

private JoinRequestStatus(String value) {
this.value = value;
}

public String getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
import com.gamzabat.algohub.common.jwt.exception.ExpiredTokenException;
import com.gamzabat.algohub.common.jwt.exception.TokenException;
import com.gamzabat.algohub.feature.comment.exception.CommentValidationException;
import com.gamzabat.algohub.feature.edgecase.exception.CannotFoundEdgeCaseException;
import com.gamzabat.algohub.feature.edgecase.exception.NotAuthorizedUserException;
import com.gamzabat.algohub.feature.group.ranking.exception.CannotFoundRankingException;
import com.gamzabat.algohub.feature.group.studygroup.exception.CannotFoundGroupException;
import com.gamzabat.algohub.feature.group.studygroup.exception.CannotFoundProblemException;
import com.gamzabat.algohub.feature.group.studygroup.exception.CannotFoundUserException;
import com.gamzabat.algohub.feature.group.studygroup.exception.GroupMemberValidationException;
import com.gamzabat.algohub.feature.group.studygroup.exception.InvalidRoleException;
import com.gamzabat.algohub.feature.group.studygroup.exception.JoinRequestException;
import com.gamzabat.algohub.feature.image.exception.AwsS3Exception;
import com.gamzabat.algohub.feature.notice.exception.NoticeValidationException;
import com.gamzabat.algohub.feature.notification.exception.CannotFoundNotificationException;
Expand Down Expand Up @@ -231,4 +234,28 @@ protected ResponseEntity<ErrorResponse> handleCannotFoundVerificationCodeExcepti
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage(), null));
}

@ExceptionHandler(CannotFoundEdgeCaseException.class)
protected ResponseEntity<ErrorResponse> handleCannotFoundEdgeCaseException(
CannotFoundEdgeCaseException e) {
return ResponseEntity
.status(e.getHttpStatus())
.body(new ErrorResponse(e.getHttpStatus().value(), e.getErrors(), null));
}

@ExceptionHandler(NotAuthorizedUserException.class)
protected ResponseEntity<ErrorResponse> handleNotAuthorizedUserException(
NotAuthorizedUserException e) {
return ResponseEntity
.status(e.getHttpStatus())
.body(new ErrorResponse(e.getHttpStatus().value(), e.getError(), null));
}

@ExceptionHandler(JoinRequestException.class)
protected ResponseEntity<ErrorResponse> handleCannotFoundVerificationCodeException(
JoinRequestException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage(), null));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.gamzabat.algohub.feature.edgecase.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.gamzabat.algohub.common.annotation.AuthedUser;
import com.gamzabat.algohub.exception.RequestException;
import com.gamzabat.algohub.feature.edgecase.domain.EdgeCaseSortType;
import com.gamzabat.algohub.feature.edgecase.dto.CreateEdgeCaseRequest;
import com.gamzabat.algohub.feature.edgecase.dto.GetEdgeCaseListResponse;
import com.gamzabat.algohub.feature.edgecase.dto.TogleEdgeCaseResponse;
import com.gamzabat.algohub.feature.edgecase.service.EdgeCaseService;
import com.gamzabat.algohub.feature.user.domain.User;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/edge-case")
@Tag(name = "반례 게시판 API", description = "반례 게시판 관련 API")
public class EdgeCaseController {
private final EdgeCaseService edgeCaseService;

@PostMapping
@Operation(summary = "반례 등록")
public ResponseEntity<Void> createEdgeCase(@AuthedUser User user,
@RequestBody @Valid CreateEdgeCaseRequest createEdgeCaseRequest,
Errors errors) {
if (errors.hasErrors())
throw new RequestException("올바르지 않은 요청입니다.", errors);

edgeCaseService.createEdgeCase(user, createEdgeCaseRequest);

return ResponseEntity.ok().build();
}

@GetMapping("/list")
@Operation(summary = "반례리스트 조회")
public ResponseEntity<GetEdgeCaseListResponse> getEdgeCaseList(
@AuthedUser(required = false) User user,
@RequestParam(required = false) Integer problemNumber,
@RequestParam(required = false, defaultValue = "RECENT") EdgeCaseSortType sort
) {
GetEdgeCaseListResponse response = edgeCaseService.getEdgeCaseList(user, problemNumber, sort);

return ResponseEntity.ok().body(response);
}

@DeleteMapping("/{edgeCaseId}")
@Operation(summary = "반례 삭제")
public ResponseEntity<Void> deleteEdgeCase(@AuthedUser User user, @PathVariable Long edgeCaseId) {
edgeCaseService.deleteEdgeCase(user, edgeCaseId);

return ResponseEntity.ok().build();
}

@PatchMapping(value = "/{edgeCaseId}/like")
@Operation(summary = "반례 좋아요 토글")
public ResponseEntity<TogleEdgeCaseResponse> addEdgeCaseLike(@AuthedUser User user, @PathVariable Long edgeCaseId) {
TogleEdgeCaseResponse result = edgeCaseService.togleEdgeCaseLike(user, edgeCaseId);

return ResponseEntity.ok().body(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.gamzabat.algohub.feature.edgecase.domain;

import java.time.LocalDateTime;

import org.hibernate.annotations.SQLDelete;

import com.gamzabat.algohub.feature.user.domain.User;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
@SQLDelete(sql = "UPDATE edge_case SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?")
public class EdgeCase {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Integer level;
private String link;
private Integer problemNumber;
private String title;
@Column(columnDefinition = "TEXT")
private String input;
@Column(columnDefinition = "TEXT")
private String output;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User author;

private LocalDateTime createdAt;
private LocalDateTime deletedAt;

@Column(nullable = false)
private int likeCount = 0;

@Builder
public EdgeCase(Integer level, String link, Integer problemNumber, String title, String input, String output,
User author) {
this.level = level;
this.link = link;
this.problemNumber = problemNumber;
this.title = title;
this.input = input;
this.output = output;
this.author = author;
this.createdAt = LocalDateTime.now();
}

public void increaseLikeCount() {
this.likeCount++;
}

public void decreaseLikeCount() {
if (this.likeCount > 0) {
this.likeCount--;
}
}
}
Loading
Loading