Today's Codekata
// 로또의 최고 순위와 최저 순위
class Solution {
public int[] solution(int[] lottos, int[] win_nums) {
int[] answer = new int[2];
int high = 0;
int low = 0;
for (int i = 0; i < lottos.length; i++) {
if (lottos[i] == 0) {
high++;
}
for (int j = 0; j < win_nums.length; j++) {
if (lottos[i] == win_nums[j]) {
high++;
low++;
}
}
}
int[] rank = {6, 6, 5, 4, 3, 2, 1};
answer[0] = rank[high];
answer[1] = rank[low];
return answer;
}
}
// 옹알이(2)
class Solution {
public int solution(String[] babbling) {
int answer = 0;
String[] sounds = {"aya", "ye", "woo", "ma"};
for (String word : babbling) {
int idx = 0;
String prev = "";
boolean isValid = true;
while (idx < word.length()) {
boolean matched = false;
for (String sound : sounds) {
if (word.startsWith(sound, idx)) {
if (sound.equals(prev)) {
isValid = false;
break;
}
prev = sound;
idx += sound.length();
matched = true;
break;
}
}
if (!matched) {
isValid = false;
break;
}
}
if (isValid) answer++;
}
return answer;
}
}
옹알이(2) 문제는 정답 코드를 역으로 분석하면서 로직의 흐름과 조건 처리 방식을 이해할 수 있었다. 활용할 수 있는 것들이 더 많아져서 여러 방식중에 좋은 방식을 골라서 코딩할 수 있는 개발자가 되고 싶다.
-- 저자 별 카테고리 별 매출액 집계하기
SELECT B.AUTHOR_ID, A.AUTHOR_NAME, B.CATEGORY,
SUM(S.SALES * B.PRICE) AS TOTAL_SALES
FROM BOOK B
JOIN AUTHOR A ON B.AUTHOR_ID = A.AUTHOR_ID
JOIN
(
SELECT BOOK_ID, SALES
FROM BOOK_SALES
WHERE SALES_DATE BETWEEN '2022-01-01' AND '2022-01-31'
) S ON B.BOOK_ID = S.BOOK_ID
GROUP BY 1, 2, 3
ORDER BY AUTHOR_ID, CATEGORY DESC;
-- 대여 횟수가 많은 자동차들의 월별 대여 횟수 구하기
SELECT MONTH(H.START_DATE) AS MONTH, H.CAR_ID, COUNT(H.CAR_ID) AS RECORDS
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY H
JOIN (
SELECT CAR_ID
FROM CAR_RENTAL_COMPANY_RENTAL_HISTORY
WHERE START_DATE BETWEEN '2022-08-01' AND '2022-10-31'
GROUP BY CAR_ID
HAVING COUNT(CAR_ID) >= 5
) A ON H.CAR_ID = A.CAR_ID
WHERE MONTH(H.START_DATE) IN (8,9,10)
GROUP BY MONTH, H.CAR_ID
ORDER BY MONTH(H.START_DATE), H.CAR_ID DESC;
-- 그룹별 조건에 맞는 식당 목록 출력하기
SELECT M.MEMBER_NAME, R.REVIEW_TEXT, DATE_FORMAT(R.REVIEW_DATE, "%Y-%m-%d") AS REVIEW_DATE
FROM REST_REVIEW R
JOIN MEMBER_PROFILE M ON R.MEMBER_ID = M.MEMBER_ID
WHERE R.MEMBER_ID IN (
SELECT MEMBER_ID
FROM (
SELECT MEMBER_ID, COUNT(*) AS REVIEW_COUNT,
RANK() OVER (ORDER BY COUNT(*) DESC) AS RANKING
FROM REST_REVIEW
GROUP BY MEMBER_ID
) T
WHERE RANKING = 1
)
ORDER BY R.REVIEW_DATE, R.REVIEW_TEXT;
마지막 문제에서 `LIMIT 1`로 최다 리뷰 작성 회원을 찾는 서브쿼리를 만들었더니 오답이 나왔는데, `RANK()` 함수를 이용해서 동률까지 포함해주니 정답에 도달했다. 더 익숙해져서 때때마다 사용방법을 찾지 않고 답에 도달하고 싶다.
일정 관리 앱
일정에 달릴 댓글 구현하기
@PostMapping("/schedules/{scheduleId}/comments")
public ResponseEntity<CommentResponse> createComment(
@PathVariable Long scheduleId,
@Valid @RequestBody CreateCommentRequest request
) {
CommentResponse comment = commentService.createComment(scheduleId, request);
return new ResponseEntity<>(comment, HttpStatus.CREATED);
}
일정에 댓글을 달기 위해 위와 같은 댓글 생성 API를 만들었는데, 호출할 때 아래와 같은 오류가 계속 발생했다.

일정 ID도 유저 ID도 데이터베이스에 있는 것도 확인했으나 문제가 없었고, `requestURI`을 로그로 확인해보니 `/schedules/1/comments` 가 아닌 `/schedules/1/`이 출력되고 있었다. 도저히 원인을 찾을 수 없어서 AI한테 물어봤지만 URL을 다시 확인해보거나 GET 요청으로 보낸 것이 아닌지 확인하라는 얘기만 반복하던 중 Spring이 댓글 생성 API가 아니라 일정 단건 조회, 수정, 삭제 등에서 사용되는 `/schedules/{id}` 매핑을 시도하다 실패하고 `/error`로 넘어가고 있음을 확인했다. `{id}`에 `1/comments`가 들어가서 `Long` 변환에 실패해서 계속 오류가 발생했다고 생각이 들어서 `{id:\\d+}` 이런 정규식을 통해서 숫자로 제한해보기도 했는데, 여전히 오류가 발생한다. 그러다 우연히 Postman 콘솔을 확인해보니 이쪽 요청에서 comments가 누락되고 있었다. 정말 어이없게도 Postman을 재시작하니 정상적으로 API가 호출된다. 이 문제로 몇시간을 헤맸는데 찾아보니 굉장히 흔한 일이라고 한다. 환경 변수나 캐시, 경로 자동 완성, 쿠키 문제 등으로 한 번 꼬이면 말도 안 되는 요청을 만들어버리는 일이 종종 있다고 나와있다. 개발자라면 한번쯤 겪는 통과의례라 생각하고, 결국은 해결했으니 이것도 하나의 과정이라 생각하려고 한다.