맥북에서 스크린 캡쳐를 하다보면 지저분하게 바탕화면에 저장된다.
기본 캡쳐 폴더 변경 방법
Shift + Command + 5 를 누르자.

Option -> Other Location을 선택하자.
위와 같이 하면 다음부터는 스크린 캡쳐시 위에서 지정한 폴더에 저장될 것이다.
맥북에서 스크린 캡쳐를 하다보면 지저분하게 바탕화면에 저장된다.
Shift + Command + 5 를 누르자.

Option -> Other Location을 선택하자.
위와 같이 하면 다음부터는 스크린 캡쳐시 위에서 지정한 폴더에 저장될 것이다.

블로그를 운영하는 입장에서 과연 내가 언제 돈을 벌 수 있을까 다들 한번 쯤은 고민을 한다.
이와 관련된 좋은 글이 있어서 요약해봤다.
원문
가장 널리 알려진 방법이다. 누군가 광고를 보고 클릭하면 돈을 벌 수 있다.
개인적으로 제휴 광고는 블로그 주제와 맞는 것을 선택하는 것이 맞다고 생각한다. 예를 들어 개발자면 Udemy 강의에 대한 제휴 광고 링크를 블로그 글에 추가하는 방식이다. 내가 제품을 리뷰하는 블로거라면 아마존 제휴 광고 링크를 넣는것이 어울릴 것이다. 그리고 해당 링크를 사람들이 클릭하면 그에 대한 트래픽에 따라 돈을 주는 방식이다.
아마존의 제휴 마케팅 광고 (https://affiliate-program.amazon.com)

유데미(Udemy)의 제휴 마케팅 광고 (https://www.udemy.com/affiliate)

Trip.com의 제휴 마케팅 (https://www.trip.com/partners/index)


네이버 블로그에서 흔히 많이 보이는 “해당 포스트는 xx 로부터 협찬을 받아 작성된 글입니다” 글들이 이에 해당된다.
워드프레스에도 있는 기능인데 블로그 방문하는 사람들이 내 블로그를 구독할 수 있도록 기능을 제공하는 것이다. 방문자가 내 블로그를 구독하게 되면 내가 글을 올릴 때마다 이메일로 알람이 간다. 이것은 구독자들이 지속적으로 나의 블로그에 방문하게 만들어준다. 즉 팬을 확보하는 작업이라 할 수 있다.
개인적으로 블로그 글에 집중해서 차근차근 페이지뷰를 늘리고 난 다음에 구글 애드센스 + 제휴 광고 링크를 넣어서 수익화 하는 것이 장기적으로 봤을 때 가장 좋다고 생각한다.
지금부터 말하는 단위는 한 달 기준이다. 페이지 뷰가 늘어날 수록 애드센스 수익 외에도 다른 수익 채널을 통해 더 많은 수익을 낼 수 있다. 아래는 단순히 애드센스 수익만 고려했다는 점 참고.
유명인이 아닌 이상 블로그를 오픈하면 누구나 페이지 뷰 0으로 시작한다. 글을 꾸준히 올리면 한달에 1,000 페이지뷰에 달성하는 순간이 온다.
전체 블로거의 40%가 이 구간에 있다고 함.
이 구간에서 페이지뷰가 5,000이 넘을 경우 제휴링크를 추가해서 얻는 수익이 구글 애드센스 수익보다 많아지는 구간이다.
구글 애드센스 수익은 1,000 페이지 뷰 = 2 – 12 달러 (블로그 주제에 따라 달라질 수 있다)
예를 들어 페이지 뷰가 10,000이라면 애드센스 수익 약 20달러 – 120달러를 예상할 수 있다.
페이지뷰가 60,000이 넘는다면 https://www.mediavine.com 와 같은 업체를 통해 수익화 구조를 확장하는 것이 도움이 되는 구간이다.
애드센스 수익은 1,000 페이지 뷰당 최소 10달러이다.
페이지뷰가 100,000일 경우 애드센스 수익으로만 약 1,000달러 예상할 수 있다.
이 구간에 진입하게 된다면 스폰서 포스팅에 대한 가치가 상당히 높아져서 수익이 아주 좋다. (업체로부터 협찬을 받아서 글 쓰는 것)
위의 3가지 진짜 공감된다. 예를 들어 남들이 다 쓰는 주제는 내 블로그가 검색될 확률이 정말 낮다. 그 만큼 해당 검색결과로 나오는 블로그가 정말 많기 때문이다. 더군다나 이제 막 개설한 블로그라면 더욱 그렇다. (독서를 좋아한다면 책에 관련된 포스팅을 꾸준히 올리는 것도 분명 큰 도움이 될 것이다. 당장은 노출 안되도 언젠가는 터진다 만약 누군가가 해당 도서에 관심이 있다면..)
누구나 자신만이 알고 있는 지식과 경험이 있다. 이런 글들을 꾸준히 올리는 것이 페이지뷰를 늘리는 데 정말 큰 도움이 된다.
내 블로그 글 중에 백악관 투어 예약하기라는 글이 있다.
많은 사람들이 워싱턴 DC를 방문하지만 백악관 내부를 둘러보지는 않는다. 하지만 누구나 백악관 내부를 구경해보고 싶다는 생각을 할 수 있다. 내가 이 글을 작성할 때만 해도 정말 검색해도 나오지 않는 정보였다. 그래서 그때 당시 나는 이미 경험했고 어떻게 예약해서 갈 수 있는지 알고있기 때문에 작성했다. 그래서 결과는?

검색결과를 보면 마이리얼트립이 가장 상단에 있다. 그리고 그 위에 스폰서라고 써있다. 즉 구글에 엄청난 비용을 지불해서 첫 페이지 최상단에 노출되는 결과를 얻었다. 그리고 두번째는 워싱턴 공식 페이지이고 세번째는 여행업체이다. 그 다음 네 번째가 바로 내 블로그이다.
블로그 주소도 무료버전 티가 난다. 나만의 닷컴 주소도 아닌 wordpress.com 주소이다. 저때 당시 SEO에 대해서도 잘 몰랐다. 그저 검색해도 찾기 어려웠던 글을 올렸을 뿐인데 SEO에서 가장 중요한 구글의 첫 페이지 상단에 내 블로그 글을 노출 시켰다.
블로그로 돈 버는 방법을 썼지만 아이러니하게도 나는 아직 블로그로 돈을 번적이 없다. 내 블로그는 현재 월 페이지 뷰가 이제 1,000이 넘기 시작했다. 앞으로 싱가포르에서 소프트웨어 개발자로 일하면서 경험하는 소소한 내용들을 정리해서 포스팅을 꾸준히 할 예정이다. 많은 구독과 관심 주시면 더욱 더 열심히 포스팅 할 예정이다.

오랜만에 싱가포르 현지인 커플을 만났다.
점심을 먹고 디저트로 여기를 가보자고 해서 방문. 강추 강추 존맛탱
hvala가 카페 이름인데 글씨가 엄청 작게 써있어서 잘 봐야된다.
사이트도 있다.

주문은 키오스크로 한다


전형적인 싱가포르 가게구조. 길게 쭈욱 뻗어있는 가게들이 많다.

아이스크림도 맛있고 녹차도 엄청 맛있게 먹었다. 녹차를 주문할 때 등급이 있는데 우리는 중간 등급으로 먹었다. 아주 만족스러운 녹차라떼였다.





우리는 Craig Road 지점으로 갔었다. 싱가포르내에 여러지점이 있으니 아래 사이트 들어가서 가까운 곳 방문해보는 것을 추천함
전남대 병원 근처에 있는 카페가 있어서 찾아갔다. 결론부터 말하자면 커피를 정말 잘 하는 집이다. 나는 시그니쳐 커피를 주문했는데 굳이 비교하자면 강원도에 있는 툇마루 커피와 비슷한 메뉴이다.
툇마루 커피도 정말 맛있었지만 여기 카페도 전혀 뒤쳐지지 않았다. 아니 오히려 좀 더 맛있게 느껴졌다.
전남대 병원 근처에 올일이 있다면 꼭 가볼만한 카페다.

요 건물 건너편 쯔음 위치한 카페다

빌라가 많은 동네의 큰 길가에 있다. 가게 앞에 주차를 할 수 있지만 1대 정도만 주차할 수 있어 보였다.



드디어 내가 주문한 커피가 나왔다. 실내 공간이 협소해서 앉을 자리가 없었다. 아쉽지만 테이크 아웃을 했다.

커피맛으로만 따지자면 아주아주 훌륭했다. 다만 조금 아쉬웠던 점은 주차공간이 없다는 점과 몇 없는 실내 테이블 좌석을 몇몇 손님이 거의 독차지 할 수 밖에 없는 구조다.
예를 들어 4명이 앉을 수 있는 테이블에 손님 두명이 앉아있고 그 옆에 가방을 의자위에 올려놓고 차지하고 있었다.
이런 손님들에게 사장님이 옆에 다른 사람이 앉을 수 있도록 안내를 좀 해줬으면 좋았을 텐데 그 부분이 아쉬웠다.

2020년 결혼기념일 여행으로 싱가포르에 갔었다
그때 당시 마리나베이샌즈에서도 1박 했었는데 그 기억이 강렬해서인지 이 호텔에 대한 기억이 희미해졌다
사진첩과 여행 계획표를 에버노트에서 찾다가 우리가 묶었던 호텔이 만다린 오차드였다는 것을 확인
관련기사
참고로 이 호텔은 현재 힐튼으로 리브랜딩 중인걸로 확인했고 리노베이션 공사중인지 암튼 지금은 잠시 문 닫은 상태다



조식 맛있었던 기억이 난다. 약간 중국식? 메뉴들이 많았었다. 난 콘지를 좋아해서 이거저거 토핑해서 먹었었다.




위치도 진짜 좋고 전철이랑도 가까운 편이며 객실 청결 상태도 좋았다.

탄종파가를 다녀왔다
줄서서 먹는 빵집으로 유명한 챔피언이다. 여기는 홍콩에서 들여온 브랜드라고 한다
특히나 유명한 빵은 카레빵인데 이날 품절되서 다른 인기있는 소브르 빵을 사먹었다








빵을 좋아한다면 강추. 사람들이 줄서는 것은 다 이유가 있다

This post might be helpful if you are considering using the typeText function for validating user input using regular expressions, such as for validating passwords or usernames.
Although XCTest enables you to write unit tests for validating the results of regular expressions, it is not suitable for writing test cases that involve inputting characters one by one.
Let me show the example.
Business requirements
Username allows alphabet + numbers only
Password allows alphabet + numbers + special characters
Address allows alphabet + numbers only
Phone number format is 4 digits – 4 digits
Postal Code is 5 numbers
Birthday format is yyyyMMDD
Username, Password, Phone, Address are required fields
Error message will be displayed when entering text into the TextField
Search button will be enabled once the postal code is validated.
Let’s write a test cases
It has a sut for checking unitTests and typeText function (like a XCUIElement’s typeText) to put the characters into the active UITextField.
class ProfileInputValidatorTests: XCTestCase {
var sut: MockClass!
override func setUp() {
super.setUp()
sut = MockClass()
}
override func tearDown() {
super.tearDown()
sut.activeTextField = nil
sut.inputCheckType = nil
sut.errorMessage = nil
}
//UnitTests
}
private extension ProfileInputValidatorTests {
func typeText(_ input: String) {
guard let activeTextField = sut.activeTextField else {
return
}
for (location, character) in input.enumerated() {
let canInputText = activeTextField.delegate?.textField?(
activeTextField,
shouldChangeCharactersIn: NSRange(location: location, length: 0),
replacementString: String(character)
)
if canInputText ?? false {
let lastPosition = activeTextField.endOfDocument
activeTextField.selectedTextRange = activeTextField
.textRange(from: lastPosition, to: lastPosition)
activeTextField.insertText(String(character))
}
}
}
}
Here is the example of unit tests
func test_textField_should_not_exceed_maxCharacter() {
ProfileInputType.allCases.forEach {
sut.setActiveTextField($0)
let dummyText = """
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
"""
typeText(dummyText)
XCTAssertTrue(
sut.activeTextField?.text?.count == sut.inputValidator.activeTextFieldType?.maxCharacter,
"TextField tag: \($0.rawValue) should not exceed maxCharacter"
)
}
}
func test_username_allowance_character() {
let username = "skboard"
sut.setActiveTextField(.username)
typeText(username)
sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
XCTAssertNil(sut.errorMessage)
XCTAssertEqual(sut.inputCheckType, ProfileInputType.username)
}
func test_username_not_allowance_character() {
let username = "マーン224s한글djfタklsdf"
sut.setActiveTextField(.username)
typeText(username)
sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
XCTAssertEqual(ProfileInputType.username.errorMessage, sut.errorMessage)
XCTAssertEqual(sut.inputCheckType, ProfileInputType.username)
}
func test_postal_code_allowance_character() {
let postalCode = "13456"
sut.setActiveTextField(.postalCode)
typeText(postalCode)
sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
XCTAssertNil(sut.errorMessage)
XCTAssertEqual(sut.inputCheckType, ProfileInputType.postalCode)
}
func test_is_enabled_search_button_while_editing() {
sut.setActiveTextField(.postalCode)
let postalCode = "89761"
typeText(postalCode)
XCTAssertEqual(sut.activeTextField?.text, "89761")
XCTAssertTrue(sut.searchButton.isEnabled, "searchButton should enabled")
}
func test_is_enabled_search_button_after_didEndEditing() {
sut.setActiveTextField(.postalCode)
let postalCode = "24028"
typeText(postalCode)
XCTAssertEqual(sut.activeTextField?.text, "24028")
sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
XCTAssertTrue(sut.searchButton.isEnabled, "searchButton should enabled")
}
func test_phone_number_format() {
sut.setActiveTextField(.phone)
let phoneNumber = "78761241"
typeText(phoneNumber)
XCTAssertEqual(sut.activeTextField?.text, "7876-1241")
}
func test_birthday_format() {
let birthday = "19980115"
sut.setActiveTextField(.birthday)
typeText(birthday)
sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
XCTAssertNil(sut.errorMessage)
XCTAssertEqual(sut.inputCheckType, ProfileInputType.birthday)
}
func test_phone_number_only_allow_one_hyphen_character() {
sut.setActiveTextField(.phone)
let phoneNumber = "1467834527891471247"
typeText(phoneNumber)
let hyphenCount = numberOfHyphen(in: sut.activeTextField?.text ?? "")
XCTAssertTrue(
hyphenCount == 1,
"phone number only allow one hyphen character"
)
}
InputValidator checks the inputTypes and validates strings.
protocol ProfileInputValidatorDelegate: AnyObject {
var activeTextField: UITextField? { get set }
func setActiveTextField(_ inputType: ProfileInputType)
func updateAddressSearchButtonState(isEnabled: Bool)
func showValidateResult(_ inputType: ProfileInputType?, error message: String?)
}
final class ProfileInputValidator: NSObject {
var activeTextFieldType: ProfileInputType?
var dateFormatter: DateFormatter?
weak var delegate: ProfileInputValidatorDelegate?
func validate(
text: String,
type: ProfileInputType,
needToSetType: Bool = false,
placeholder: String? = nil
) {
guard isNotEmpty(text: text) else {
showEmptyMessage(type: type, needToSetType: needToSetType, placeholder: placeholder)
return
}
let errorMessage: String?
switch type {
case .username:
errorMessage = isValidUsername(text) ? nil : type.errorMessage
case .password:
errorMessage = isValidPassword(text) ? nil : type.errorMessage
case .phone:
errorMessage = isValidPhoneNumber(text) ? nil : type.errorMessage
case .address:
errorMessage = isValidAddress(text) ? nil : type.errorMessage
case .postalCode:
errorMessage = isValidPostalCode(text) ? nil : type.errorMessage
case .birthday:
errorMessage = isValidBirthday(text) ? nil : type.errorMessage
}
delegate?.showValidateResult(needToSetType ? type : activeTextFieldType, error: errorMessage)
}
private var isRequiredField: Bool {
if activeTextFieldType == .some(.username) ||
activeTextFieldType == .some(.password) ||
activeTextFieldType == .some(.address) ||
activeTextFieldType == .some(.phone) {
return true
}
else {
return false
}
}
private func showEmptyMessage(type: ProfileInputType, needToSetType: Bool, placeholder: String?) {
let textFieldTitle = activeTextFieldType?.title ?? ""
var emptyErrorMessage: String?
if isRequiredField {
emptyErrorMessage =
"\(placeholder ?? textFieldTitle) is required"
}
delegate?.showValidateResult(
needToSetType ? type : activeTextFieldType,
error: emptyErrorMessage
)
}
private func isNotEmpty(text: String?) -> Bool {
guard let inputText = text else {
return false
}
let textWithOutWhiteSpace = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
return !textWithOutWhiteSpace.isEmpty
}
private func isValidDateFormat(text: String) -> Bool {
guard convertBirthdayFormat(text)?.count == ProfileInputType.birthday.maxCharacter else {
return false
}
return true
}
}
This class conforms to UITextFieldDelegate to check strings
//MARK: UITextFieldDelegate
extension ProfileInputValidator: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
guard let fieldType = ProfileInputType(textField.tag) else {
return
}
delegate?.activeTextField = textField
activeTextFieldType = fieldType
}
func textFieldDidEndEditing(_ textField: UITextField) {
guard let inputText = textField.text, let inputType = activeTextFieldType else {
return
}
validate(text: inputText, type: inputType)
}
func textField(
_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String
) -> Bool {
let text = textField.text ?? ""
guard let stringRange = Range(range, in: text),
let activeTextFieldMaxCount = activeTextFieldType?.maxCharacter
else {
return false
}
if activeTextFieldType == .phone, let phoneNumber = formattingPhoneNumber(text: text, to: string, range: range) {
textField.text = phoneNumber
return false
}
let updatedText = text.replacingCharacters(in: stringRange, with: string)
if activeTextFieldType == .postalCode {
delegate?.updateAddressSearchButtonState(isEnabled: updatedText.count == activeTextFieldMaxCount)
}
return updatedText.count <= activeTextFieldMaxCount
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard let activeType = activeTextFieldType else {
return false
}
textField.endEditing(false)
// Focus on the next field
switch activeType {
case .username:
delegate?.setActiveTextField(.password)
case .password:
delegate?.setActiveTextField(.phone)
case .phone:
delegate?.setActiveTextField(.address)
case .address:
delegate?.setActiveTextField(.postalCode)
case .postalCode:
delegate?.setActiveTextField(.birthday)
default:
break
}
return false
}
}
This class has a InputValidator and conforms to ProfileInputValidatorDelegate. It is use for XCTestCase.
class MockClass {
var activeTextField: UITextField?
var searchButton = UIButton()
var inputCheckType: ProfileInputType?
var errorMessage: String?
fileprivate let inputValidator = ProfileInputValidator()
var usernameTextField: UITextField!
var passwordTextField: UITextField!
var addressTextField: UITextField!
var phoneTextField: UITextField!
var postalCodeTextField: UITextField!
var birthdayTextField: UITextField!
lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy.MM.dd"
formatter.calendar = Calendar(identifier: .gregorian)
return formatter
}()
init() {
inputValidator.delegate = self
inputValidator.dateFormatter = dateFormatter
setupTextFields()
searchButton.isEnabled = false
}
private func setupTextFields() {
usernameTextField = UITextField()
passwordTextField = UITextField()
addressTextField = UITextField()
phoneTextField = UITextField()
postalCodeTextField = UITextField()
birthdayTextField = UITextField()
let textFields = [
usernameTextField,
passwordTextField,
addressTextField,
phoneTextField,
postalCodeTextField,
birthdayTextField
]
for (index, textField) in textFields.enumerated() {
textField?.delegate = inputValidator
textField?.tag = index
}
}
}
extension MockClass: ProfileInputValidatorDelegate {
func setActiveTextField(_ inputType: ProfileInputType) {
switch inputType {
case .username:
usernameTextField.delegate?.textFieldDidBeginEditing?(usernameTextField)
case .password:
passwordTextField.delegate?.textFieldDidBeginEditing?(passwordTextField)
case .address:
addressTextField.delegate?.textFieldDidBeginEditing?(addressTextField)
case .phone:
phoneTextField.delegate?.textFieldDidBeginEditing?(phoneTextField)
case .postalCode:
postalCodeTextField.delegate?.textFieldDidBeginEditing?(postalCodeTextField)
case .birthday:
birthdayTextField.delegate?.textFieldDidBeginEditing?(birthdayTextField)
}
}
func updateAddressSearchButtonState(isEnabled: Bool) {
searchButton.isEnabled = isEnabled
}
func showValidateResult(_ inputType: ProfileInputType?, error message: String?) {
errorMessage = message
inputCheckType = inputType
}
}
XCTest is a powerful framework for testing unit tests, but it can be challenging to test UI-related logic. In mobile development, UI logic is closely intertwined with the user interface itself. The conventional approach for testing UI logic is to use XCUITest. However, XCUITest tends to be slower compared to XCTest. If we have multiple views designed for registering or updating information using input fields, we would need to write multiple UI testing code with XCUITest.
An alternative approach is to use XCTest and create a function that simulates user actions, such as typing text. This allows us to write a unit test for common input validator logic while considering user actions as well. This way, we can test the UI-related logic using XCTest in a more efficient manner compared to relying solely on XCUITest.
애플 홈팟 미니를 선물 받았다






간단하게 전원을 연결하고 아이폰을 근처로 가져다두면 팝업이 뜬다. 추가하기 버튼 누르면 애플 홈에 스피커가 추가된다
음질 만족스럽고 디자인도 완전 맘에 든아
여기는 외식비가 비싸다. 레스토랑 같은 곳에서 먹으면 GST라고 음식값에 세금이 붙고 추가적으로 서비스 비용이 붙는 경우도 있다.
그래서 실제 결제 금액은 메뉴판에 있는 금액 보다 더 붙는다
아무튼 비싼 외식 값 때문에 집에서 자주 요리를 한다. 오늘은 보쌈 만드는 방법 간략하게 정리
참고로 아래 조리법은 백종원 유투브 그대로 따라한 것이다.
어디서 사도 상관은 없는데 나는 주로 Fair Price 슈퍼의 정육점 코너로 가서 1인분 인 경우 주로 150-200 그램 (가격은 약 5-7달러), 2인분인 경우 300그램 호주산 삼겹살 포크벨리를 구매한다.

양파 반조각
파
된장 2 스푼

사진이 좀 흔들렸다. 아무튼 삼겹살이 잠길정도로 물을 붓고 양파랑 파 그리고 된장 넣고 한 30-45분 끓인다
물이 끓는 동안 소스를 만든다. 일단 새우젓이 필요한데 이건 한인마트인 고려마트에서 구매했다. 그리고 고추가루는 국산 강추. 이것도 고려마트에서 팔지만 나는 장모님이 주신거 가져왔다
준비물
새우젓 크게 2-3 스푼
청양고추 잘게 썰기
한국산 고추가루 1스푼
설탕 두스푼
간마늘 (이건 Fair Price에서 중국산으로 구매)

진짜 보쌈은 요리하는거 엄청 쉽다. 한번 도전해보는 것을 강력추천한다
나는 특히 새우젓 소스랑 같이 먹는 것이 너무 맛있었다. (백종원 유투브 최고)
아 그리고 보쌈 끓이고 남은 물 버리지말고 두부 사다가 넣어먹으면 아주 훌륭한 된장찌게가 된다. (육수국물이기 때문에 맛있다)
반응 좋아요 많으면 제육볶음 등 다른 요리도 올릴 예정

예전에 구매했던 강의를 다시 보고 있다. 운동을 좀 다시 체계적으로 해보려고 한다.
아무튼 Udemy에서 헬스 관련 쪽으로 거의 탑인 강사가 알려주는 팁을 정리해보고자 한다.
탄수화물 Carbohydrate
프로틴 (근육생성)
지방 (Fats)
근육 생성 공식 (40% 탄수화물, 30% 프로틴, 30% 지방 섭취)
운동 전후 음식 (전후 30분 섭취 권장)
그 외에 추천
프로틴
지방
탄수화물
까먹을까봐 정리하는 유데미 강의 노트 정리
You must be logged in to post a comment.