포슀트

HP Trivia (1)

πŸ“ Folder vs Group (Xcode)

ν•­λͺ©Folder (νŒŒλž€μƒ‰) ImageGroup (νšŒμƒ‰) Image
κΈ°λ³Έ 적용 버전Xcode 16λΆ€ν„° κΈ°λ³ΈXcode 15 μ΄ν•˜μ—μ„œ κΈ°λ³Έ
파일 μ‹œμŠ€ν…œ λ°˜μ˜μ‹€μ œ macOS 파일 μ‹œμŠ€ν…œμ—λ„ λ™μΌν•œ 폴더 ꡬ쑰둜 생성됨Xcode ν”„λ‘œμ νŠΈ λ‚΄μ—μ„œλ§Œ μ‘΄μž¬ν•˜λ©°, μ‹€μ œ 파일 μ‹œμŠ€ν…œκ³Ό μΌμΉ˜ν•˜μ§€ μ•Šμ„ 수 있음
Git 병합 μΆ©λŒμƒλŒ€μ μœΌλ‘œ μ μŒν”„λ‘œμ νŠΈ 파일 (.xcodeproj) 자체 변경이 λ§Žμ•„μ Έ 좩돌 μœ„ν—˜ λ†’μŒ
ν”„λ‘œμ νŠΈ λ‚΄ 정렬이름 κΈ°μ€€ μžλ™ μ •λ ¬ (μ•ŒνŒŒλ²³ 순)μˆ˜λ™ μ •λ ¬ κ°€λŠ₯ (개발자 자유 배치)
ν˜‘μ—… μ‹œ μž₯점Git 병합 좩돌 μ΅œμ†Œν™”λ‘œ ν˜‘μ—…μ— μœ λ¦¬ν˜‘μ—… μ‹œ ν”„λ‘œμ νŠΈ 파일 좩돌 κ°€λŠ₯μ„± 큼
λ‹¨μ νŒŒμΌ μœ„μΉ˜κ°€ 고정적이며, μ»€μŠ€ν…€ μ •λ ¬ 어렀움Git 좩돌 μ‹œ 해결이 κΉŒλ‹€λ‘­κ³ , μ‹€μˆ˜ κ°€λŠ₯μ„± 있음

κ°•μ˜μ—μ„  Git μΆ©λŒμ„ 쀄이기 μœ„ν•΄ Folder 방식을 μ„ ν˜Έ
ν•˜μ§€λ§Œ 개인적으둜 파일 정렬을 더 μœ μ—°ν•˜κ²Œ κ΄€λ¦¬ν•˜κ³  μ‹Άλ‹€λ©΄ Group 방식도 κ³ λ €ν•  수 있음


Single Responsibility Principle (단일 μ±…μž„ 원칙)

μ •μ˜
ν•˜λ‚˜μ˜ 클래슀, ꡬ쑰체, λ·°, 파일 등은 였직 ν•˜λ‚˜μ˜ μ±…μž„λ§Œμ„ κ°€μ Έμ•Ό ν•˜λ©°, ν•˜λ‚˜μ˜ 이유둜만 λ³€κ²½λ˜μ–΄μ•Ό ν•œλ‹€λŠ” μ†Œν”„νŠΈμ›¨μ–΄ 섀계 원칙.


μ˜ˆμ‹œ

λ‚˜μœ 예

1
2
3
4
5
6
7
struct CurrencyView: View {  
    var body: some View {  
        // 톡화 μ•„μ΄μ½˜ λ””μžμΈ  
        // κ·Έλ¦¬λ“œ λ ˆμ΄μ•„μ›ƒ 처리  
        // 톡화 선택 ν™”λ©΄ 전체 ꡬ성  
    }  
}

λͺ¨λ“  역할이 ν•˜λ‚˜μ˜ 뷰에 λͺ°λ € μžˆμ–΄ μœ μ§€λ³΄μˆ˜ 및 μž¬μ‚¬μš© 어렀움

쒋은 예

1
2
3
4
5
6
7
8
9
10
11
struct CurrencyIconView: View {  
    // 톡화 μ•„μ΄μ½˜ λ””μžμΈλ§Œ λ‹΄λ‹Ή  
}

struct IconGridView: View {  
    // μ—¬λŸ¬ 개의 μ•„μ΄μ½˜μ„ κ·Έλ¦¬λ“œλ‘œ 배치  
}

struct SelectCurrencyView: View {  
    // 전체 화면을 κ΅¬μ„±ν•˜λ©° μœ„ μ»΄ν¬λ„ŒνŠΈλ₯Ό μ‘°ν•©  
}

각 λ·°λŠ” ν•œ κ°€μ§€ μ—­ν• λ§Œ ν•˜λ©°, λͺ…ν™•ν•œ μ±…μž„ 뢄리


μž₯점

  • μ½”λ“œ 가독성 ν–₯상
  • μœ μ§€λ³΄μˆ˜ 용이
  • ν…ŒμŠ€νŠΈ λ‹¨μœ„ μž‘μ•„μ§ β†’ μœ λ‹› ν…ŒμŠ€νŠΈ 용이
  • μž¬μ‚¬μš©μ„±κ³Ό ν™•μž₯μ„± ν–₯상

μœ μ—°ν•˜κ²Œ μ μš©ν•˜μž

λ°˜λ“œμ‹œ μ—„κ²©νžˆ μ μš©ν•  ν•„μš”λŠ” μ—†μŒ.
상황과 ν”„λ‘œμ νŠΈμ˜ 성격에 맞게 μ μš©ν•˜λ©΄ 됨.
핡심은 β€œνŒŒμΌ/ꡬ쑰체/λ·°κ°€ λ„ˆλ¬΄ λ§Žμ€ 역할을 ν•˜μ§€ μ•Šλ„λ‘ ν•˜λŠ” 것”

μ—¬κΈ°κΉŒμ§€κ°€ κ°•μ˜μ˜ μ΄ˆλ°˜μ„ μ •λ¦¬ν•œ λ‚΄μš©.

ν”„λ‘œμ νŠΈμ—μ„œλŠ”

Image Image


폴더 κ΅¬μ„±ν•˜κΈ°

μ΄λ²ˆμ—λŠ” κΈ°μ‘΄ ν”„λ‘œμ νŠΈμ™€ 달리 폴더λ₯Ό λ§Œλ“€κ³  swiftνŒŒμΌλ“€μ„ κ΄€λ¦¬ν•˜λ €κ³  ν•œλ‹€.

Image

이후, ν”„λ‘œμ νŠΈμ— ν•„μš”ν•œ νŒŒμΌμ„ λ„£μ–΄μ£Όμž.

μ΄λ²ˆμ—λ„ μ—­μ‹œ Swift 버전은 6으둜.

λͺ¨λΈλ§

μ΄λ²ˆμ—λ„ μ—­μ‹œ json을 μ‚¬μš©ν•˜κΈ°μ— λͺ¨λΈλ§μ„ ν•΄μ£Όλ„λ‘ν•œλ‹€.

1
2
3
4
5
6
7
8
struct Question: Decodable {
    let id: Int
    let question: String
    let answer: String
    let wrong: [String]
    let book: Int
    let hint: String
}

λͺ¨λΈλ§μ€ λ‹€μŒκ³Ό κ°™λ‹€.


μ•± κ΅¬ν˜„ ν”Œλžœ μ„Έμš°κΈ°

μ΄λ²ˆμ—λŠ” μ²˜μŒλΆ€ν„° ν”Œλžœμ„ μ„Έμš°κ³  λ“€μ–΄κ°„λ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
App Development Plan:
- Game Intro Screen
- Gameplay Screen
- Game logic (questions, scores, etc.)
- Celebration
- Audio
- Animations
- In-app purchases
- Store
- Instructions screen
- Books
- Persist scores

κΈ°λŠ₯을 λ‚˜μ—΄μ„ ν•΄λ³΄μ•˜λ‹€ (μˆœμ„œλŠ” 무관)

μ•„λ§ˆλ„ κ²°κ³ΌλŠ” μ΄λ ‡κ²Œ λ‚˜μ˜¬λ“―

Image


Instruction View λ””μžμΈ

Image

λ”±νžˆ μ–ΈκΈ‰ν•  뢀뢄이 μ—†μ–΄μ„œ 이건 전체 μ½”λ“œλ‘œ λŒ€μ²΄ν•œλ‹€.

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
struct InstructionsView: View {
    @Environment(\.dismiss) private var dismiss
    
    
    var body: some View {
        ZStack {
            Image(.parchment)
                .resizable()
                .ignoresSafeArea()
                .background(.brown)
            
            VStack {
                Image(.appiconwithradius)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 100)
                    .padding(.top)
                
                ScrollView {
                    Text("How To Play")
                        .font(.largeTitle)
                        .padding()
                    
                    VStack(alignment: .leading, spacing: 20) {
                        Text("Welcome to HP Trivia! In this game you will be asked random questions from the HP books and you must guess the right answer or you will lose points!😱")
                        
                        Text("Each question is worth 5 points, but if you guess a wrong answer, you lose 1 point.")
                        
                        Text("If you are struggling with a question, there is an option to reveal a hint or reveal the book that answers the question. But beware! Using these also removes 1 point each.")
                        
                        Text("When you select the correct answer, you will be awarded all the points left for that question and they will be added to your total score.")
                        
                    }
                    .font(.title3)
                    .padding(.horizontal)
                    
                    Text("Good Luck!")
                        .font(.title)
                }
                
                Button("Done") {
                    dismiss()
                }
                .font(.largeTitle)
                .padding()
                .buttonStyle(.borderedProminent)
                .tint(.brown.mix(with: .black, by: 0.2))
                .foregroundStyle(.white)
            }
       

μ΄λ ‡κ²Œ κΈ°μ΄ˆμž‘μ—…μ€‘ ν•˜λ‚˜κ°€ 끝났닀.

PhaseAnimator

Image

μš°μ„  μ΄λ ‡κ²Œ λ””μžμΈμ„ ν•΄μ€€λ‹€.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ContentView: View {
    var body: some View {
        GeometryReader { geo in
            ZStack {
                Image(.hogwarts)
                    .resizable()
                    .frame(width: geo.size.width * 3, height: geo.size.height)
                    .padding(.top, 3)
            }
            .frame(width: geo.size.width, height: geo.size.height)
        }
        .ignoresSafeArea()
    }
}

μ½”λ“œλŠ” μ΄λ ‡κ²Œ μž‘μ„±.

이제 μ—¬κΈ°μ„œ 쑰금 더 UIλ₯Ό λ°œμ „μ‹œν‚€κΈ°μœ„ν•΄ phaseAnimator Modifierλ₯Ό μ‚¬μš©ν•œλ‹€.

padding 밑에 μΆ”κ°€ν•΄μ£Όμ—ˆλ‹€.

1
2
3
4
5
6
7
.padding(.top, 3)
.phaseAnimator([false, true]) { content, phase in
    content
        .offset(x: phase ? geo.size.width/1.1 : -geo.size.width/1.1)
} animation: { _ in
        .linear(duration: 60)
}

μ‹€ν–‰ν•˜λ©΄ μ•„λž˜μ™€κ°™λ‹€. (β€» μ•„λž˜ μ˜ˆμ‹œλŠ” duration을 ν…ŒμŠ€νŠΈμš©μœΌλ‘œ 2초둜 μ„€μ •)

Image

이렇과 쒌우둜 움직인닀.

  • phaseAnimator ?
    • phaseAnimatorλŠ” SwiftUIμ—μ„œ μ‚¬μš©ν•˜λŠ” λ·° μ• λ‹ˆλ©”μ΄μ…˜ λ„κ΅¬λ‘œ, μ—¬λŸ¬ μƒνƒœ(phase) κ°„ μ „ν™˜μ„ μžλ™μœΌλ‘œ λ°˜λ³΅ν•˜λ©΄μ„œ μ• λ‹ˆλ©”μ΄μ…˜μ„ μ μš©ν•  수 μžˆλ‹€. Docs}λŠ” μ—¬κΈ°
  • μ‚¬μš© λͺ©μ 
    • νŠΉμ • λ·° 속성을 μƒνƒœλ³„λ‘œ λ³€ν™”μ‹œμΌœ 반볡 μ• λ‹ˆλ©”μ΄μ…˜ 생성
    • 예: μ΄λ―Έμ§€μ˜ 쒌우 이동, 투λͺ…도 λ³€ν™”, 크기 λ³€ν™” λ“±
Parametersμ„€λͺ…
phasesμˆœν™˜ν•  μƒνƒœ(phase)λ“€μ˜ μ‹œν€€μŠ€. λΉ„μ–΄ 있으면 λŸ°νƒ€μž„ 경고와 μ‹œκ°μ  κ²½κ³ κ°€ λ°œμƒν•¨
content두 개의 인자λ₯Ό λ°›λŠ” λ·° λΉŒλ” ν΄λ‘œμ €: μˆ˜μ •λœ λ·° ν”„λ‘μ‹œμ™€ ν˜„μž¬ phase
animationphase κ°„ μ „ν™˜ μ‹œ μ‚¬μš©ν•  μ• λ‹ˆλ©”μ΄μ…˜μ„ λ°˜ν™˜ν•˜λŠ” ν΄λ‘œμ €. nil이면 μ• λ‹ˆλ©”μ΄μ…˜ μ—†μŒ

κ°•μ˜μ—μ„œλŠ” 일반적으둜 2~3개의 μƒνƒœλ₯Ό μ‚¬μš©ν•˜λŠ” 것이 일반적이라고 ν•œλ‹€. 그리고 [false, true]λ₯Ό μ‚¬μš©ν•œ μ΄μœ λŠ” Apple의 곡식 예제 λŒ€λΆ€λΆ„μ΄ false β†’ true의 νλ¦„μœΌλ‘œ κ΅¬μ„±λ˜μ–΄ 있기 λ•Œλ¬Έμ΄λ‹€λΌκ³  ν•œλ‹€.

Transition Animation

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
struct ContentView: View {
    @State private var animateViewsIn = false // new

    var body: some View {
        GeometryReader { geo in
            ZStack {
                // μƒλž΅

                VStack {
                    VStack {
                        if animateViewsIn {
                            VStack {
                                Image(systemName: "bolt.fill")
                                    .imageScale(.large)
                                    .font(.largeTitle)

                                Text("HP")
                                    .font(.custom("PartyLetPlain", size: 70))
                                    .padding(.bottom, -50)

                                Text("Trivia")
                                    .font(.custom("PartyLetPlain", size: 60))
                            }
                            .padding(.top, 70)
                            .transition(.move(edge: .top))
                        }
                    }
                    .animation(.easeInOut(duration: 0.7).delay(2), value: animateViewsIn)
                    
                    Spacer()
                }
            }
            .frame(width: geo.size.width, height: geo.size.height)
        }
        .ignoresSafeArea()
        .onAppear {
                animateViewsIn = true
        }
    }
}

μ΄λ ‡κ²Œ μ½”λ“œλ₯Ό κ°μ„±ν–ˆλ‹€.

  • Transition
    • SwiftUIμ—μ„œ .transition(.move(edge: .top))은 λ·°κ°€ 화면에 λ“±μž₯ν•˜κ±°λ‚˜ μ‚¬λΌμ§ˆ λ•Œ μ• λ‹ˆλ©”μ΄μ…˜ 효과λ₯Ό μ μš©ν•˜λŠ” 데 μ‚¬μš©λœλ‹€. 단, λ‹€μŒ 쑰건이 μΆ©μ‘±λ˜μ–΄μ•Ό μ œλŒ€λ‘œ μž‘λ™ν•œλ‹€:
      • λ·°λŠ” if 쑰건을 톡해 μ‚½μž…/μ‚­μ œλ˜μ–΄μ•Ό 함
      • ν•΄λ‹Ή ifλ₯Ό κ°μ‹ΈλŠ” μ™ΈλΆ€ 뷰에 .animation(_:value:)이 μžˆμ–΄μ•Ό 함
  • Transition μž‘λ™μ„ μœ„ν•œ ν•„μˆ˜ ꡬ성 μš”μ†Œ
    1. @State둜 Boolean λ³€μˆ˜ μ„ μ–Έ
    2. .onAppearμ—μ„œ μƒνƒœκ°’ λ³€κ²½ (withAnimation μ‚¬μš©ν•˜κ±°λ‚˜ μ™ΈλΆ€μ—μ„œ .animation() 적용)
    3. if 문으둜 λ·° 쑰건 λΆ„κΈ°
    4. .transition(.move(edge: .top)) 적용
    5. .animation(_:value:)을 if문을 감싼 μ™ΈλΆ€ VStack에 적용

Group λŒ€μ‹  VStack을 μ“΄ 이유
Group은 μ—¬λŸ¬ λ·°λ₯Ό λ¬ΆλŠ” 데 μ“°μ΄λŠ” κ²½λŸ‰ μ»¨ν…Œμ΄λ„ˆμ΄μ§€λ§Œ, μ• λ‹ˆλ©”μ΄μ…˜ νŠΈλ¦¬κ±°μ™€ κ΄€λ ¨ν•΄ λ‹€μŒκ³Ό 같은 λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆλ‹€:

  1. λ‚΄λΆ€ μƒνƒœ λ³€ν™” 감지가 λΆˆμ•ˆμ •ν•  수 있음
  2. .animation(_:value:)을 뢙여도 κΈ°λŒ€ν•œ λŒ€λ‘œ λ™μž‘ν•˜μ§€ μ•Šμ„ 수 있음
    κ·Έλž˜μ„œ κ°•μ˜μ—μ„œλŠ” Group λŒ€μ‹  VStack을 μ‚¬μš©ν•˜μ—¬ 더 μ•ˆμ •μ μœΌλ‘œ μž‘λ™ν•˜κ²Œ ν–ˆλ‹€.

μ½”λ“œλ₯Ό 보며 κ°„λ‹¨νžˆ μž‘λ™μ— λŒ€ν•΄ μ•Œμ•„λ³΄κΈ°

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GeometryReader { geo in
    ZStack {
        // μƒλž΅
        VStack {
            VStack {
                if animateViewsIn {
                    VStack {
                        // μƒλž΅
                    }
                    .padding(.top, 70)
                    .transition(.move(edge: .top))
                }
            }
            .animation(.easeInOut(duration: 0.7).delay(2), value: animateViewsIn)

            Spacer()
        }
    }
    .frame(width: geo.size.width, height: geo.size.height)
}
.ignoresSafeArea()
.onAppear {
        animateViewsIn = true
}
  • .transition()은 if 내뢀에 μžˆλŠ” λ·°(VStack)에 적용됨
    • μ—¬κΈ°μ„  μƒλ‹¨μ—μ„œ μ›€μ§μ΄κ² λ‹€λŠ” 것.
  • .animation(_:value:)은 ifλ₯Ό κ°μ‹ΈλŠ” μ™ΈλΆ€ VStack에 μ μš©λ˜μ–΄μ•Ό ν•˜λ©°, ν•΄λ‹Ή VStack이 λ‚˜νƒ€λ‚  λ•Œ easeInOut μ• λ‹ˆλ©”μ΄μ…˜μ΄ 2초 뒀에 싀행됨
  • ifλŠ” Viewκ°€ μ•„λ‹ˆλ―€λ‘œ modifierλ₯Ό 뢙일 수 μ—†κ³ , wrapping이 ν•„μš”ν•¨
    • κ°•μ˜μ—μ„œλŠ” Group λŒ€μ‹  VStack을 μ‚¬μš©ν•˜μ—¬ μ•ˆμ •μ μΈ μ• λ‹ˆλ©”μ΄μ…˜ 처리λ₯Ό μœ λ„ν•¨
  • .onAppearμ—μ„œ μƒνƒœκ°’μ„ λ³€κ²½ν•˜μ—¬ μ• λ‹ˆλ©”μ΄μ…˜μ΄ 트리거됨

즉, value에 μ „λ‹¬λœ μƒνƒœ(animateViewsIn)κ°€ falseμ—μ„œ onAppearλ•Œ true둜 λ³€κ²½λ˜λ©΄μ„œ animation이 트리거되고, ν•΄λ‹Ή μ‹œμ μ— if λ‚΄λΆ€μ˜ VStack이 μƒμ„±λ˜λ©΄μ„œ μœ„μ—μ„œ λ‚΄λ €μ˜€λŠ” μ• λ‹ˆλ©”μ΄μ…˜κ³Ό ν•¨κ»˜ 화면에 λ‚˜νƒ€λ‚œλ‹€.

그리고 ν˜Ήμ‹œλ‚˜ μ΄ˆκΈ°κ°’μ„ true둜 μ„€μ •ν•˜λ©΄ !animateViewsIn처럼 λΆ€μ • 쑰건을 써야 ν•΄μ„œ μ½”λ“œ 흐름이 μ§κ΄€μ μœΌλ‘œ μ½νžˆμ§€ μ•Šκ³ , μ „ν™˜ μ‹œμ λ„ λͺ…ν™•ν•˜μ§€ μ•Šλ‹€.

μ‹€ν–‰ν•˜λ©΄ μ•„λž˜μ™€ 같이 λœλ‹€.

Image


.animation vs withAnimation

κ°‘μžκΈ° 문득 κ°„λ‹¨νžˆ μ •λ¦¬ν•˜λ©΄ 쒋을것 κ°™μ•„ 여기에 적어본닀.


  • withAnimation
    • SwiftUI의 ν•¨μˆ˜ 기반 μ• λ‹ˆλ©”μ΄μ…˜ 트리거
    • λ‚΄λΆ€ ν΄λ‘œμ €μ—μ„œ λ°œμƒν•˜λŠ” μƒνƒœ λ³€ν™”κ°€ λ·°λ₯Ό λ°”κΎΈλ©΄, ν•΄λ‹Ή 변화에 μ• λ‹ˆλ©”μ΄μ…˜ 적용
    • 주둜 onAppear, onTapGesture, Button μ•‘μ…˜ λ“± μ΄λ²€νŠΈμ„± λ™μž‘κ³Ό ν•¨κ»˜ μ‚¬μš©λ¨

μ˜ˆμ‹œ:

1
2
3
withAnimation(.easeInOut) {
    isVisible.toggle()
}
  • isVisible의 λ³€κ²½μœΌλ‘œ 인해 if isVisible 쑰건 ν•˜μ˜ λ·°κ°€ μ‚½μž… λ˜λŠ” 제거되면, ν•΄λ‹Ή 뷰에 transition이 μ μš©λ˜μ–΄ μ• λ‹ˆλ©”μ΄μ…˜ λ°œμƒ

  • .animation(_:value:)
    • SwiftUI의 modifier 기반 선언적 μ• λ‹ˆλ©”μ΄μ…˜
    • νŠΉμ • μƒνƒœκ°’(value)의 λ³€ν™”κ°€ ν•΄λ‹Ή λ·° 트리의 μ™Έν˜• λ³€ν™”λ‘œ μ΄μ–΄μ§ˆ 경우, μ• λ‹ˆλ©”μ΄μ…˜ 적용
    • if 쑰건이 λ°”λ€ŒλŠ” κ²ƒμ²˜λŸΌ, 뷰의 λ“±μž₯/퇴μž₯이 쑰건에 따라 λ°”λ€ŒλŠ” 상황에 적합

μ˜ˆμ‹œ:

1
.animation(.easeInOut, value: isVisible)
  • isVisible이 λ°”λ€” λ•Œ, 이 값을 κΈ°μ€€μœΌλ‘œ λ·° 트리 λ³€ν™”κ°€ 생기면 transitionκ³Ό ν•¨κ»˜ μ• λ‹ˆλ©”μ΄μ…˜ 처리

μš”μ•½

ν•­λͺ©withAnimation.animation(_:value:)
νƒ€μž…ν•¨μˆ˜modifier
μœ„μΉ˜μƒνƒœ 변경을 κ°μ‹ΈλŠ” ν΄λ‘œμ €View νŠΈλ¦¬μ— 직접 적용
μ£Ό μš©λ„μ΄λ²€νŠΈ 기반 μ• λ‹ˆλ©”μ΄μ…˜ νŠΈλ¦¬κ±°μƒνƒœ λ³€ν™” 기반 λ·° μ—…λ°μ΄νŠΈ 감지
이 κΈ°μ‚¬λŠ” μ €μž‘κΆŒμžμ˜ CC BY 4.0 λΌμ΄μ„ΌμŠ€λ₯Ό λ”°λ¦…λ‹ˆλ‹€.