📌 미션 - 로또

🔍 진행 방식

  • 미션은 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항 세 가지로 구성되어 있다.
  • 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
  • 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.

🚀 기능 요구 사항

로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다.

1
2
3
4
- 로또 번호의 숫자 범위는 1~45까지이다.
- 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
- 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다. - 1등: 6개 번호 일치 / 2,000,000,000원 - 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 - 3등: 5개 번호 일치 / 1,500,000원 - 4등: 4개 번호 일치 / 50,000원 - 5등: 3개 번호 일치 / 5,000원
  • 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
  • 로또 1장의 가격은 1,000원이다.
  • 당첨 번호와 보너스 번호를 입력받는다.
  • 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.
  • 사용자가 잘못된 값을 입력할 경우 throw문을 사용해 예외를 발생시키고, “[ERROR]”로 시작하는 에러 메시지를 출력 후 종료한다.

입출력 요구 사항

입력

  • 로또 구입 금액을 입력 받는다. 구입 금액은 1,000원 단위로 입력 받으며 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다.
1
14000
  • 당첨 번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다.
1
1,2,3,4,5,6
  • 보너스 번호를 입력 받는다.
1
7

출력

  • 발행한 로또 수량 및 번호를 출력한다. 로또 번호는 오름차순으로 정렬하여 보여준다.
1
2
3
4
5
6
7
8
9
8개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
[3, 5, 11, 16, 32, 38]
[7, 11, 16, 35, 36, 44]
[1, 8, 11, 31, 41, 42]
[13, 14, 16, 38, 42, 45]
[7, 11, 30, 40, 42, 43]
[2, 13, 22, 32, 38, 45]
[1, 3, 5, 14, 22, 45]
  • 당첨 내역을 출력한다.
1
2
3
4
5
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
  • 수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%)
1
총 수익률은 62.5%입니다.
  • 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 “[ERROR]”로 시작해야 한다.
1
[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.

실행 결과 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
구입금액을 입력해 주세요.
8000

8개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
[3, 5, 11, 16, 32, 38]
[7, 11, 16, 35, 36, 44]
[1, 8, 11, 31, 41, 42]
[13, 14, 16, 38, 42, 45]
[7, 11, 30, 40, 42, 43]
[2, 13, 22, 32, 38, 45]
[1, 3, 5, 14, 22, 45]

당첨 번호를 입력해 주세요.
1,2,3,4,5,6

보너스 번호를 입력해 주세요.
7

당첨 통계

3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
총 수익률은 62.5%입니다.

🎯 프로그래밍 요구 사항

  • Node.js 14 버전에서 실행 가능해야 한다. Node.js 14에서 정상적으로 동작하지 않을 경우 0점 처리한다.
  • 프로그램 실행의 시작점은 App.js의 play 메서드이다. 아래와 같이 프로그램을 실행시킬 수 있어야 한다.

예시

1
2
const app = new App();
app.play();
  • package.json을 변경할 수 없고 외부 라이브러리(jQuery, Lodash 등)를 사용하지 않는다. 순수 Vanilla JS로만 구현한다.
  • JavaScript 코드 컨벤션을 지키면서 프로그래밍 한다
  • 프로그램 종료 시 process.exit()를 호출하지 않는다.
  • 프로그램 구현이 완료되면 ApplicationTest의 모든 테스트가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.
  • 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.
  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    • 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
  • 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
  • Jest를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.

추가된 요구 사항

  • 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.
  • else를 지양한다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
    • 때로는 if/else, switch문을 사용하는 것이 더 깔끔해 보일 수 있다. 어느 경우에 쓰는 것이 적절할지 스스로 고민해 본다.
  • 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(Console.readLine, Console.print) 로직에 대한 단위 테스트는 제외한다.
    • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
    • 단위 테스트 작성이 익숙하지 않다면 tests/LottoTest.js를 참고하여 학습한 후 테스트를 구현한다.

라이브러리

  • MissionUtils 라이브러리에서 제공하는 Random 및 Console API를 사용하여 구현해야 한다.
    • Random 값 추출은 MissionUtils 라이브러리의 Random.pickUniqueNumbersInRange()를 활용한다.
    • 사용자의 값을 입력 받고 출력하기 위해서는 MissionUtils 라이브러리에서 제공하는 Console.readLine, Console.print를 활용한다.

사용 예시

1
const numbers = MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6);

Lotto 클래스

  • 제공된 Lotto 클래스를 활용해 구현해야 한다.
  • numbers의 # prefix를 변경할 수 없다.
  • Lotto에 필드를 추가할 수 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Lotto {
#numbers;

constructor(numbers) {
this.validate(numbers);
this.#numbers = numbers;
}

validate(numbers) {
if (numbers.length !== 6) {
throw new Error();
}
}

// TODO: 추가 기능 구현
}

회고

🐥 당첨 번호 및 로또 번호 예외 처리

  • 당첨번호 에러 처리할 때 중복검사 뿐만 아니라 범위도 확인해줘야 한다. 게다가 보너스 번호는 당첨번호와 중복 검사도 추가로 해줘야 한다.

🦉 typeof NaN === ‘number’

  • “4r”이라는 문자열을 숫자로 형변환 해주면 NaN 값이 나오게된다. 에러처리를 할 때 type이 ‘number’인지를 확인해주도록 하였는데, NaN은 number 타입을 가지고 있으므로 이에 대한 처리를 추가해줘야했다.

🦆 하드코딩 vs 소프트코딩

  • score 객체에 3등 4등 5등 의 키 값은 숫자로만 주었는데, bonus 등수는 문자열로 주어서 출력시 5등 4등 3등 2등 1등 Bonus등수가 출력되어서 이를 해결하기 위해 score의 key 값을 문자열로 변경하였다.

  • lotto 배열의 요소도 [1, 2, 3, 4, 5, 6] 이런 형태의 배열인데, print 결과물이 “[1, 2, 3, 4, 5, 6]” 이렇게 나와야 하므로 이에 대한 처리를 해주었다.

  1. 백틱만 사용
1
this.Lotto.forEach((lotto) => MissionUtils.Console.print(`${lotto}`));

Error1

  1. 문자열에 [ ] 삽입
1
this.Lotto.forEach((lotto) => MissionUtils.Console.print(`[${lotto}]`));

Error2

  1. 문자열 배열처럼 처리
1
2
3
this.Lotto.forEach((lotto) =>
MissionUtils.Console.print(`[${lotto.join(", ")}]`)
);

Error3

🐒 숫자를 천 단위 콤마찍은 문자열로 변환하기

  • Number.prototype.toLocaleString() 메서드를 사용하면 천단위에 ,를 찍은 문자열로 변환한다.

✏️ 피드백 (22.11.17 추가)

비즈니스 로직과 UI로직 구분

  • 한 클래스에서 담당하지 않고 구분하여야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Lotto {
#numbers

// 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
contains(numbers) {
...
}

// UI 로직
print() {
...
}
}

객체의 상태 접근 제한

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 내 코드
class App {
constructor() {
this.LottoCount = null;
this.Lotto = [];
this.winNum = [];
this.bonusNum = null;
this.score = { "3개": 0, "4개": 0, "5개": 0, bonus개: 0, "6개": 0 };
this.prize = {
"3개": 5000,
"4개": 50000,
"5개": 1500000,
bonus개: 30000000,
"6개": 2000000000,
};
this.totalMoney = null;
}
}
  • 위 와 같은 경우 객체의 데이터를 외부에서 접근 가능하므로 이는 디버깅과 유지보수를 어렵게하므로 접근 제한자를 설정하고 선택된 메서드만으로 데이터를 참조하거나 변경할 수 있도록 개선한다.

객체는 객체답게 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Lotto {
#numbers

constructor(numbers) {
this.#numbers = numbers
}

getNumbers() {
return this.#numbers
}
}

class LottoGame {
play() {
const lotto = new Lotto(...)

// 숫자가 포함되어 있는지 확인한다.
lotto.getNumbers().contains(number)
// 당첨 번호와 몇 개가 일치하는지 확인한다.
lotto.getNumbers().stream()...
}
}
  • 로또 클래스는 로또 숫자를 가져오는 역할만 한다.
  • 로또게임 클래스는 play() 메서드를 호출 하였을 경우 이와 관련된 로직으로 구성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Lotto {
#numbers

constructor(numbers) {
this.#numbers = numbers
}

contains(number) {
// 숫자가 포함되어 있는지 확인한다.
return ...
}

matchCount(other) {
// 당첨 번호와 몇 개가 일치하는지 확인한다.
return ...
}
}

class LottoGame {
play() {
const lotto = new Lotto(...)

lotto.contains(number)
lotto.matchCount(...)
}
}
  • getter를 사용하여 값을 꺼내 사용하는 대신 다른 객체에 메시지를 건네주자. 객체가 스스로 일하도록 하는 객체 지향 프로그래밍 기법을 사용하자.
  • 출력을 위한 값, 순수 값 프로퍼티를 가져오기 위해서라면 getter를 허용한다.

필드 수를 줄이도록 하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 리팩터링 전
class LottoResult {
#result = new Map()
#profitRate
#totalPrize
}

// 리팩터링 후
class LottoResult {
#result = new Map()

calculateProfitRate() { ... }

calculateTotalPrize() { ... }
}
  • 위 객체의 profitRate와 totalPrize는 등수 별 당첨 내역(result)만 있어도 모두 구할 수 있는 값이다. 따라서 위 객체는 다음과 같이 하나의 필드만으로 구현할 수 있다.

🏓 소감

2주차때 클래스구조와 테스트에 대해서 조금은 익숙해져서 3주차 미션을 받았을 때 구조가 비슷하여 조금 마음이 놓였다.

  • jest expect 함수와 matcher 함수를 찾아보면서 단위 테스트 기능에 대해 공부해볼 수 있는 기회여서 좋았다.
  • 로또 클래스와 비즈니스 로직 클래스를 구분하여 테스트를 진행하고 관심사를 분리하면서 개발을 하니 유지보수가 편리하다는 것을 느낄 수 있었다.

댓글 공유

📌 문제 및 요구사항

숫자 야구 게임 바로가기

✏️ 회고

❗️ 클래스 필드를 사용할 것인가? constructor() 안에서 정의할 것인가?

  • App이라는 클래스는 야구게임 App이다. 야구 게임의 룰을 변경하지 않는 이상 인스턴스를 생성할 때마다 초기화할 값이 없다. 그러므로 초기화 하지 않는 프로퍼티 값들은 클래스 필드에 정의해두었다.

❗️ 요구사항 제대로 읽기

  • 요구사항 제대로 읽지 않아서 라이브러리 사용하여 랜덤값 구하면 되는데, 랜덤값 구하는 로직 직접 짜서 다시 처음부터 시작하게되었다.
  • 또 제대로 안읽어서 특정 메서드 사용하여 구하라했는데 라이브러리에서 제공하는 다른 메서드로 구현하여 다시 작성했다…

❗️jest를 통한 테스트 케이스는 어떻게 확인할 수 있는지 알게되었다.

  • 주어진 라이브러리를 활용하여 1번은 테스트 하는 것을 알 수 있었다. 그런데 2번째 값을 얻기 위해서 내가 해주어야 하는 것이 무엇인지 모르겠다.

내가 직접 예상되는 값을 생각하여 테스트에 입력값만 적어주고 소스코드에서는 해당 입력값이 넘어왔다가 가정하고 그에 대한 처리를 해주면 되는 것이라는 것을 알게되었다.

❗️랜덤값 받은 것을 프로퍼티로 저장해두자

함수형 프로그래밍을 하면서 함수를 호출하고 반환된 값을 다른 함수에게 전달하는 방식으로 랜덤값과 입력값을 비교하고 결과를 반환하는 방식으로 하였는데, 이 방법보다는 App 이라는 클래스가 랜덤값과 입력값을 가져 한곳에서 이 데이터를 가져다가 사용하는 것이 적절하다고 판단하여 위와 같이 프로퍼티로 저장해두었다.

❗️형변환 확인하기

테스트 케이스를 작성할 때, answers 배열의 요소를 문자열로 넣어준 것을 제대로 확인하지 않아 왜 자꾸 문제가 발생하는지 제대로 파악하기 어려웠다. 아마도 Jest를 사용한 디버깅이 익숙하지 않은 탓이었다. 때문에 여기서 의외로 많은 시간을 보냈다. 단위 테스트를 자주 하면서 Jest 디버깅에도 익숙해지도록 노력해야겠다.

🏓 소감

저번주에 이어 2주차에 접어들게 되었다. 학원이 끝나고 혹은 주말에 틈틈히 문제를 풀었는데도 익숙하지 않는 Jest 테스트 방법 때문에 해결방법도 모른체로 삽질(?) 을 많이했던 한주였다.

그래도 주변에 물어도보고 검색도 하면서 구현까지는 어찌저찌 완성하였다.

하지만 리팩터링에 시간을 많이 쏟지 못한 것이 아쉬웠다. 우테코 문제 푸는 것도 중요하지만 당장 리액트 수업도 복습해야하고, CS 공부도 해야하고 알고리즘 문제도 풀어야하고 해야할 일이 너무 많다.

하지만 꾸준히 계획을 세워서 해나갈 것이니 언젠간 좋은 날이 올 것이라고 믿는다.

댓글 공유

🚀 기능 요구 사항

레벨 2의 팀 프로젝트 미션으로 SNS(Social Networking Service)를 만들고자 하는 팀이 있다. 팀에 속한 크루 중 평소 알고리즘에 관심이 많은 미스터코는 친구 추천 알고리즘을 구현하고자 아래와 같은 규칙을 세웠다.

  • 사용자와 함께 아는 친구의 수 = 10점
  • 사용자의 타임 라인에 방문한 횟수 = 1점

사용자 아이디 user와 친구 관계를 담은 이차원 배열 friends, 사용자 타임 라인 방문 기록 visitors가 매개변수로 주어질 때, 미스터코의 친구 추천 규칙에 따라 점수가 가장 높은 순으로 정렬하여 최대 5명을 return 하도록 solution 메서드를 완성하라. 이때 추천 점수가 0점인 경우 추천하지 않으며, 추천 점수가 같은 경우는 이름순으로 정렬한다.

제한사항

  • user는 길이가 1 이상 30 이하인 문자열이다.
  • friends는 길이가 1 이상 10,000 이하인 배열이다.
  • friends의 각 원소는 길이가 2인 배열로 [아이디 A, 아이디 B] 순으로 들어있다.
    • A와 B는 친구라는 의미이다.
    • 아이디는 길이가 1 이상 30 이하인 문자열이다.
  • visitors는 길이가 0 이상 10,000 이하인 배열이다.
  • 사용자 아이디는 알파벳 소문자로만 이루어져 있다.
  • 동일한 친구 관계가 중복해서 주어지지 않는다.
  • 추천할 친구가 없는 경우는 주어지지 않는다.

실행 결과 예시

user friends visitors result
“mrko” [ [“donut”, “andole”], [“donut”, “jun”], [“donut”, “mrko”], [“shakevan”, “andole”], [“shakevan”, “jun”], [“shakevan”, “mrko”] ] [“bedi”, “bedi”, “donut”, “bedi”, “shakevan”] [“andole”, “jun”, “bedi”]

내 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function problem7(user, friends, visitors) {
if (
typeof user !== "string" ||
!Array.isArray(friends) ||
!Array.isArray(visitors)
)
throw new Error(
"user는 문자열, friends는 배열, visitors는 배열이여야 합니다."
);
if (!user.length || user.length > 30)
throw new RangeError("user는 길이가 1 이상 30 이하인 문자열이여야 합니다.");
if (!friends.length || friends.length > 10000)
throw new RangeError(
"friends는 길이가 1 이상 10,000 이하인 배열이여야 합니다."
);
if (visitors.length < 0 || visitors.length > 10000)
throw new RangeError(
"visitors는 길이가 0 이상 10,000 이하인 배열이여야 합니다."
);

// 유저의 친구들
let userFriends = [];
friends.forEach((friend) => {
if (friend.includes(user)) userFriends = [...userFriends, ...friend];
});
userFriends = userFriends.filter((friend) => friend !== user);

// 유저 친구의 친구들 => 알 수도 있는 친구들
let mayKnowUsers = [];
friends.forEach((friend) => {
userFriends.forEach((userFriend) => {
if (friend.includes(userFriend))
mayKnowUsers = [...mayKnowUsers, ...friend];
});
});

mayKnowUsers = mayKnowUsers.filter((mayKnowUser) => {
return mayKnowUser !== user && !userFriends.includes(mayKnowUser);
});

// 유저 점수 계산
const userCount = {};
mayKnowUsers.forEach((mayKnowUser) => {
userCount[mayKnowUser] = (userCount[mayKnowUser] ?? 0) + 10;
});

visitors.forEach((visitor) => {
if (visitor === user || userFriends.includes(visitor)) return;
userCount[visitor] = (userCount[visitor] ?? 0) + 1;
});

// 점수 기준 내림차순 정렬
const sortFunc = (userA, userB) => {
const numA = userCount[userA];
const numB = userCount[userB];

if (numA - numB > 0) return -1;
else if (numA - numB < 0) return 1;
else return userA < userB ? -1 : userA === userB ? 0 : 1;
};

const userId = Object.keys(userCount);

return userId.sort((userA, userB) => sortFunc(userA, userB)).slice(0, 5);
}
  • 유저의 친구들을 구하였다.
  • 유저의 친구들의 친구들을 구하였다. ⇒ 10점을 매기기 위해
  • 방문객과 유저의 친구의 친구들을 순회하면서 점수를 매겼다.
  • 점수를 기준으로 내림차순 정렬과 동일 점수일 때 알파벳 순으로 정렬을 해주었다.

🏓 소감

이 문제는 처음에 한국말을 이해하기 어려워서 오랫동안 고전했던 문제이다. 친구에게 문제를 차근히 설명해보면서 요구사항에 대해 파악할 수 있었다.

  • 시간이 부족하여 6번문제와 7번문제는 리팩터링을 많이 못한 것이 아쉬웠다.
  • 이전까지 sort() 메서드는 단순히 오름차순, 내림차순 정렬에만 사용되는 것이라고 생각했는데, 이번 기회에 sort() 메서드는 두개의 인자를 받아서 -1, 0, 1 값을 return 하는 것을 기준으로 배열의 index를 직접 변경하는 메서드이라는 것을 제대로 알게되었다.

댓글 공유

🚀 기능 요구 사항

우아한테크코스에서는 교육생(이하 크루) 간 소통 시 닉네임을 사용한다. 간혹 비슷한 닉네임을 정하는 경우가 있는데, 이러할 경우 소통할 때 혼란을 불러일으킬 수 있다.

혼란을 막기 위해 크루들의 닉네임 중 같은 글자가 연속적으로 포함 될 경우 해당 닉네임 사용을 제한하려 한다. 이를 위해 같은 글자가 연속적으로 포함되는 닉네임을 신청한 크루들에게 알려주는 시스템을 만들려고 한다.

신청받은 닉네임 중 같은 글자가 연속적으로 포함 되는 닉네임을 작성한 지원자의 이메일 목록을 return 하도록 solution 메서드를 완성하라.

제한사항

  • 두 글자 이상의 문자가 연속적으로 순서에 맞추어 포함되어 있는 경우 중복으로 간주한다.
  • 크루는 1명 이상 10,000명 이하이다.
  • 이메일은 이메일 형식에 부합하며, 전체 길이는 11자 이상 20자 미만이다.
  • 신청할 수 있는 이메일은 email.com 도메인으로만 제한한다.
  • 닉네임은 한글만 가능하고 전체 길이는 1자 이상 20자 미만이다.
  • result는 이메일에 해당하는 부분의 문자열을 오름차순으로 정렬하고 중복은 제거한다.

실행 결과 예시

forms result
[ [“jm@email.com“, “제이엠”], [“jason@email.com“, “제이슨”], [“woniee@email.com“, “워니”], [“mj@email.com“, “엠제이”], [“nowm@email.com“, “이제엠”] ] [“jason@email.com“, “jm@email.com“, “mj@email.com“]

내 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const getPartialStr = (str) => {
const res = [];
let limit = 2;
while (limit <= str.length) {
const output = [];
let index = 0;
while (true) {
const seperated = [...str].slice(index, index + limit).join("");
index += 1;
if (limit > seperated.length) break;
output.push(seperated);
}
limit += 1;
res.push(...output);
}

return res;
};

const getDuplicatedUser = (arr) => {
let index = 0;
let position = 1;
const res = [];
while (index !== arr.length - 1) {
const set = new Set([...arr[index], ...arr[position]]);
if (set.size !== arr[index].length + arr[position].length) {
res.push(...arr[index].slice(-1), ...arr[position].slice(-1));
}
position += 1;
if (position === arr.length) {
index += 1;
position = index + 1;
}
}
return [...new Set(res)];
};

function problem6(forms) {
if (!forms.length || forms.length > 10000)
throw new RangeError("크루는 1명 이상 10,000명 이하이여야 합니다.");

const users = {};
forms.forEach((form) => {
users[form[1]] = form[0];
});
const nicknames = forms.map((nickname) => nickname[1]);
const partialStrs = nicknames.map((nickname) => getPartialStr(nickname));
const selectedUsers = getDuplicatedUser(partialStrs);

return selectedUsers.map((selectedUser) => users[selectedUser]).sort();
}
  • 닉네임의 첫번째 글자부터 2글자 이상으로 만들 수 있는 모든 부분 문자열을 구하였다.
  • 각 닉네임의 부분 문자열 배열을 비교하여 중복요소가 하나라도 있으면 중복 문자 포함 유저 배열에 등록하였다.
  • 해당 유저의 이메일을 오름차순으로 정렬한 배열을 반환하였다.

🏓 소감

우선 이 문제를 풀면서 가장 큰 성장을 이뤄냈다고 자축하고 싶었다. 왜냐하면 이와 비슷한 부분 문자열 문제를 못풀어서 스트레스를 받았던 적이 있는데 이번 기회에 나름의 방법(?)을 찾아서 구현에 성공하여 기분이 좋았다.

  • 2글자 이상으로 만들 수 있는 부분 문자열을 모두 구했기 때문에 코드가 매우 복잡해졌다. 하지만 2글자 부분 문자열만 구하여 서로 비교하여도 충분히 문제가 없을 상황이기 때문에 이 부분이 좀 아쉬웠다.
  • while 문을 사용할 경우와 for 문을 사용할 경우가 애매했었는데, 이번에 코드리뷰를 진행하면서 while 문은 특정 조건이 변경되는 상황이라면 사용하도록 하고 for 문은 일정 횟수를 반복해야 하는 상황에서 사용하도록 한다는 것을 알게 되어 상황에 맞게 적절히 사용할 수 있을 것 같다.

댓글 공유

🚀 기능 요구 사항

계좌에 들어있는 돈 일부를 은행에서 출금하고자 한다. 돈 담을 지갑이 최대한 가볍도록 큰 금액의 화폐 위주로 받는다.

돈의 액수 money가 매개변수로 주어질 때, 오만 원권, 만 원권, 오천 원권, 천 원권, 오백원 동전, 백원 동전, 오십원 동전, 십원 동전, 일원 동전 각 몇 개로 변환되는지 금액이 큰 순서대로 배열에 담아 return 하도록 solution 메서드를 완성하라.

제한사항

  • money는 1 이상 1,000,000 이하인 자연수이다.

실행 결과 예시

money result
50237 [1, 0, 0, 0, 0, 2, 0, 3, 7]
15000 [0, 1, 1, 0, 0, 0, 0, 0, 0]

내 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function problem5(money) {
if (!money || money > 1000000)
throw new RangeError(
"매개변수는 1 이상 1,000,000 이하의 자연수여야 합니다."
);

const papers = [50000, 10000, 5000, 1000, 500, 100, 50, 10, 1];
let paperCount = [];

papers.forEach((paper) => {
paperCount = [...paperCount, Math.floor(money / paper)];
money %= paper;
});

return paperCount;
}
  • 나머지와 몫을 이용해 반복문을 통해 구현하였다.

🏓 소감

  • map 메서드는 배열의 요소에 콜백함수를 호출하여 그 반환값으로 새로운 배열을 만드는 메서드라고만 생각하고 처음에 papers 배열을 map 메서드를 사용하여 map 메서드 내부에서 money를 변형시켜 비순수한 함수로 구현하였다. 하지만 함수는 외부 변수를 변경하지 않고 순수하도록 작성해야하므로 이를 forEach 메서드로 변경하였다.
  • 여기서는 parseInt 대신에 Math.floor() 메서드를 사용하여 정수로 변형시켜주었다.

댓글 공유

🚀 기능 요구 사항

어느 연못에 엄마 말씀을 좀처럼 듣지 않는 청개구리가 살고 있었다. 청개구리는 엄마가 하는 말은 무엇이든 반대로 말하였다.

엄마 말씀 word가 매개변수로 주어질 때, 아래 청개구리 사전을 참고해 반대로 변환하여 return 하도록 solution 메서드를 완성하라.

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
Z Y X W V U T S R Q P O N M L K J I H G F E D C B A

제한사항

  • word는 길이가 1 이상 1,000 이하인 문자열이다.
  • 알파벳 외의 문자는 변환하지 않는다.
  • 알파벳 대문자는 알파벳 대문자로, 알파벳 소문자는 알파벳 소문자로 변환한다.

실행 결과 예시

word result
“I love you” “R olev blf”

내 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function problem4(word) {
if (typeof word !== 'string') throw new TypeError('매개변수는 문자열 타입이여야 합니다.');
if (!word.length || word.length > 1000) throw new RangeError('매개변수는 1 이상 1,000 이하의 문자열이여야 합니다.');

const ALPHABET_LENGTH = 26;
const ASCII_NUMBER_CODE_UPPER_A = 65;
const ASCII_NUMBER_CODE_UPPER__Z = 90 ;
const ASCII_NUMBER_CODE_LOWER_A = 97;
const ASCII_NUMBER_CODE_LOWER__Z = 122;

const splitWords = [...word];
const uppers = Array.from({length:ALPHABET_LENGTH}, (_,i) => String.fromCharCode(ASCII_NUMBER_CODE_UPPER_A+i));
const lowers = Array.from({length:ALPHABET_LENGTH}, (_,i) => String.fromCharCode(ASCII_NUMBER_CODE_LOWER_A+i));

const changeOppositeStr = str => {
const codePosition = str.charCodeAt();

return (codePosition >= ASCII_NUMBER_CODE_UPPER_A && codePosition <= ASCII_NUMBER_CODE_UPPER__Z
? [...uppers].reverse()[uppers.indexOf(str)]
: codePosition >= ASCII_NUMBER_CODE_LOWER_A && codePosition <= ASCII_NUMBER_CODE_LOWER__Z
? [...lowers].reverse()[lowers.indexOf(str)]
: str);
}

return splitWords.map(splitedWord => changeOppositeStr(splitedWord)).join('');
}
  • ASCII 코드를 활용하여 AZ, az 를 담은 배열을 생성하였다.
  • map 메서드를 사용하여 각 요소를 반대편 문자열로 바꿔주었다.

🏓 소감

  • 리팩터링을 하기 전 코드는 ASCII 코드를 상수에 할당하지 않고 그대로 비즈니스 로직에 가져다가 사용하지 해당 숫자가 무엇을 의미하는지 파악하기 힘들어 상수에 할당하니 가독성이 높아졌다.
  • changeOppositeStr() 함수 내부에서 reverse()[] 이 부분이 중복되어 함수로 추출하려 했지만, 짧은 코드에 너무 많은 함수가 오히려 더 가독성을 해친다고 판단하여 그대로 두었다.
  • 로직을 ASCII 코드 대신 정규표현식으로 구현하였다면 비즈니스 로직이 훨씬 더 간결해질 것 같기에 다음에 문자열 문제가 나온다면 정규표현식으로도 도전해봐야겠다.

댓글 공유

🚀 기능 요구 사항

배달이가 좋아하는 369게임을 하고자 한다. 놀이법은 1부터 숫자를 하나씩 대면서, 3, 6, 9가 들어가는 숫자는 숫자를 말하는 대신 3, 6, 9의 개수만큼 손뼉을 쳐야 한다.

숫자 number가 매개변수로 주어질 때, 1부터 number까지 손뼉을 몇 번 쳐야 하는지 횟수를 return 하도록 solution 메서드를 완성하라.

제한사항

  • number는 1 이상 10,000 이하인 자연수이다.

실행 결과 예시

number result
13 4
33 14

내 코드

1
2
3
4
5
6
7
8
9
10
11
12
function problem3(number) {
if (number <= 0 || number > 10000 || number !== parseInt(number)) throw new Error('매개변수는 1이상 10,000 이하의 자연수만 입력 가능합니다.');

let clapCount = 0;
for (let i = 1; i < number + 1; i++) {
const splitStr = [...String(i)];

splitStr.forEach(elem => clapCount = elem === '3' || elem === '6' || elem === '9' ? clapCount + 1 : clapCount);
}

return clapCount;
}
  • 매개변수만큼 반복문을 돌면서 숫자 1부터 문자열로 변환한 다음 문자열에 3,6,9가 있으면 clapCount를 1 증가시켜주는 코드를 작성하였다.

🏓 소감

  • parseInt() 메서드 대신 +number를 사용하여 형변환을 시켰으면 어떨까하는 아쉬움이 남는다. 왜냐하면 parseInt() 메서드는 특정 진수법에 해당하는 정수로 반환하는 역할을 하기에 단순히 숫자로 형변환을 위함이 목적이라면 의미가 벗어난다고 생각했다. 마찬가지로 String() 메서드 대신 (i + ‘’)로 형변환 시켜야겠다.
  • 수강생들과 코드리뷰를 하면서 느낀점은 문자열을 다룰 때, 정규표현식을 사용할 수 있다면 좀 더 가독성이 좋은 코드를 작성할 수 있을 것 이라는 생각이 들었다. 지금부터라도 문자열 문제가 나온다면 정규표현식으로 풀어보는 연습을 해야겠다.

댓글 공유

🚀 기능 요구 사항

암호문을 좋아하는 괴짜 개발자 브라운이 이번에는 중복 문자를 이용한 새로운 암호를 만들었다. 예를 들어 “browoanoommnaon”이라는 암호문은 다음과 같은 순서로 해독할 수 있다.

  1. “browoanoommnaon”
  2. “browoannaon”
  3. “browoaaon”
  4. “browoon”
  5. “brown”

임의의 문자열 cryptogram이 매개변수로 주어질 때, 연속하는 중복 문자들을 삭제한 결과를 return 하도록 solution 메서드를 완성하라.

제한사항

  • cryptogram은 길이가 1 이상 1000 이하인 문자열이다.
  • cryptogram은 알파벳 소문자로만 이루어져 있다.

실행 결과 예시

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function problem2(cryptogram) {
if (cryptogram !== cryptogram.toLowerCase()) throw new TypeError('매개변수는 소문자만 입력할 수 있습니다.')
if (!cryptogram.length || cryptogram.length > 1000) throw new RangeError('매개변수는 1자 이상 1000자 이하만 입력할 수 있습니다.')

let strArr = [...cryptogram];
let duplicatedStrPosition = [];
let hasDuplicatedStr = true;

while(hasDuplicatedStr) {
strArr.forEach((_, i, init) => {
if (init[i] === init[i+1]) duplicatedStrPosition = [i, ...duplicatedStrPosition];
})

hasDuplicatedStr = !duplicatedStrPosition.length ? false : true;

duplicatedStrPosition = duplicatedStrPosition.filter(elem => {
strArr = [...strArr.slice(0, elem), ...strArr.slice(elem + 2)];
return false;
})
}

return strArr.join('');
}
  • strArr로 문자열을 배열로 관리
  • strArr 배열이 더 이상 중복이 없을 때 까지 반복해야하고, 문자열의 순회하며 중복을 확인해야 하므로 시간 복잡도 $n^2$
  • Spread문법으로 원본 배열의 변형을 최소화하는 방향으로 배열을 관리
  • for문 대신 배열 메서드 forEach문 사용
  • 원본 배열을 변형시키는 splice 메서드 대신 slice 메서드 사용

🏓 소감

  • 내가 코드를 처음 보는 사람의 입장으로 변수명부터 잘 이해할 수 있는지 고려하여 리팩터링을 하였다.
  • 처음에는 while 조건식을 단순히 true라고 하였는데, 의미를 부여하여 코드의 가독성을 높이기 위해 변수에 할당해주었다.
  • 배열을 다루기 때문에 for문 대신 forEach문을 사용하였다.
  • splice 메서드 같이 원본을 변형시키는 메서드 대신 spread 문법과 slice 메서드를 사용하였다.
  • 리팩터링 전에는 duplicatedStrPosition 배열을 직접 빈 배열로 초기화해주었지만, 리팩터링을 하면서 filter 메서드를 사용하여 좀 더 배열을 다루는 의미를 부여하였다.

하나 걸리는 부분은 제한사항을 에러처리를 해주어야 하나 고민을 하였다. 사용자가 제한사항에 벗어나는 입력을 할 수도 있으므로 이에 대한 에러를 발생시켜주는 것이 사용성에 더 옳다고 생각하여 if 조건문을 통해 에러처리를 해주었습니다.

에러처리에 대해서는 깊게 생각해보지 못했는데, 제한사항에 대해 한번 더 깊이 생각해보면서 에러처리의 필요성을 느낄 수 있었던 문제여서 즐거웠다.

또한 stack 자료구조를 생각하면서 문제를 해석하면 지금의 코드보다 훨씬 더 간결하고 가독성있는 코드를 구현할 수 있다는 것을 오늘 코드리뷰를 통해 깨닫게 되었다. stack으로 구현해보는 것은 다음 시간에 해보도록 하자.

댓글 공유

📌 문제

포비와 크롱이 페이지 번호가 1부터 시작되는 400 페이지의 책을 주웠다. 책을 살펴보니 왼쪽 페이지는 홀수, 오른쪽 페이지는 짝수 번호이고 모든 페이지에는 번호가 적혀있었다. 책이 마음에 든 포비와 크롱은 페이지 번호 게임을 통해 게임에서 이긴 사람이 책을 갖기로 한다. 페이지 번호 게임의 규칙은 아래와 같다.

  1. 책을 임의로 펼친다.
  2. 왼쪽 페이지 번호의 각 자리 숫자를 모두 더하거나, 모두 곱해 가장 큰 수를 구한다.
  3. 오른쪽 페이지 번호의 각 자리 숫자를 모두 더하거나, 모두 곱해 가장 큰 수를 구한다.
  4. 2~3 과정에서 가장 큰 수를 본인의 점수로 한다.
  5. 점수를 비교해 가장 높은 사람이 게임의 승자가 된다.
  6. 시작 면이나 마지막 면이 나오도록 책을 펼치지 않는다.
  7. 포비와 크롱이 펼친 페이지가 들어있는 배열 pobi와 crong이 주어질 때, 포비가 이긴다면 1, 크롱이 이긴다면 2, 무승부는 0, 예외사항은 -1로 return 하도록 solution 메서드를 완성하라.

제한사항

  • pobi와 crong의 길이는 2이다.
  • pobi와 crong에는 [왼쪽 페이지 번호, 오른쪽 페이지 번호]가 순서대로 들어있다.

실행 결과 예시

pobi crong result
[97, 98] [197, 198] 0
[131, 132] [211, 212] 1
[99, 102] [211, 212] -1

나의 해설

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function problem1(pobi, crong) {
const [pobiLeft, pobiRight] = pobi;
const [crongLeft, crongRight] = crong;

if (pobiRight - pobiLeft !== 1 || crongRight - crongLeft !== 1) return -1;

const sum = (str) =>
str
.split("")
.map((v) => +v)
.reduce((acc, cur) => acc + cur, 0);
const multiply = (str) =>
str
.split("")
.map((v) => +v)
.reduce((acc, cur) => acc * cur, 1);

const getMax = (left, right) => {
const leftMax = Math.max(sum(String(left)), multiply(String(left)));
const rightMax = Math.max(sum(String(right)), multiply(String(right)));
return Math.max(leftMax, rightMax);
};

const pobiMax = getMax(pobiLeft, pobiRight);
const crongMax = getMax(crongLeft, crongRight);

return pobiMax > crongMax ? 1 : pobiMax < crongMax ? 2 : 0;
}
  1. 우선 자주 사용될 것 같은 left 페이지 수와 right 페이지 수를 배열 구조분해 할당으로 변수에 할당해주었다.

  2. 이후 예외가 등장했을 때, 빠르게 결과를 내기 위해 바로 예외 처리를 해주었다.

  3. 각 페이지의 자릿수의 합과 곱을 구하는 함수를 생성해주었다.

  4. left와 right의 결과값 중 높은 값을 구하는 함수를 생성하였다.

  5. pobi의 최댓값과 crong의 최댓값을 비교하여 결과를 반환하였다.

🏓 소감

우아한 테크코스 프리코스 첫 문제를 받아보고 내가 이 문제를 푼 방법을 나열하며 정리해보았다. 논리의 흐름을 정리하니 다음번에 비슷한 문제가 나오면 더 빠른 시간 내에 풀고 다양한 시도를 해볼 수 있을 것 같다.

앞으로 4주간 프리코스가 진행될텐데 합격하든 불합격하든 나의 성장을 위해 모든 문제에 대한 풀이를 작성해보도록 할 것이다.

댓글 공유

  • page 1 of 1

loco9939

author.bio


author.job