AI 탐구노트

퀴즈 : 국기보고 나라 이름 맞추기 게임 만들기 본문

DIY 테스트

퀴즈 : 국기보고 나라 이름 맞추기 게임 만들기

42morrow 2024. 10. 2. 15:50

 

국기 모양을 보고 나라 이름 맞추기를 하는 간단한 프로그램을 만들어 볼까 합니다.

아이와 같이 놀 수 있는 장난감을 만드는거죠.

 

예상되는 과정은 다음과 같습니다.

 

1.국가 별 국기 이미지 및 국가명 수집 

국가코드를 파일명으로 가진 svg, png 타입의 파일이 필요합니다.

직접 그릴 수는 없으니 만들어진 것 가운데 저작권 등 문제가 없는 것을 찾아보려고 합니다.

 

2.국가코드로 국가명 매칭하는 코드 작성

예를 들어 br.svg 파일은 브라질의 국기이고, ch.svg는 스위스의 국기입니다.

국기를 보여주고 국가명을 맞추는 것이니 국가명이 한국어와 영어로 매핑되도록 테이블을 생성해야 합니다.

 

3.퀴즈 프로그램 작성 

복잡하지 않게 웹 페이지 하나로 만들어서 바로 사용할 수 있도록 할 계획입니다. 

 

 

 

1.국가 별 국기 이미지, 국가 코드 수집

국가마다 각자의 국기 이미지가 있죠.

국가별 국기 이미지는 저작권, 디자인권, 상표권 등으로 보호되지 않아 특별한 이용 허락 없이 사용 가능하다고 합니다. 

 

그럼 우선 많이 모아두고 제공해주는 곳을 찾아보기로 했습니다. 

흠... 공공데이터포털 (data.go.kr)에 가면 외교부가 보유하고 있는 국가별 (196개국)의 국기 이미즈를 다운받을 수 있다고 하네요.

 

그런데...

공공데이터포털에서 제공되는 국기 이미지 압축파일을 받아서 확인해 보고 놀랐습니다. 

외교부에서 공식적으로 보유하고 있는 국가 별 국기 데이터가 글쎄 다음과 같았습니다. 

  • 파일 형식 : GIF
    문제 : 직사각형이 아닌 국기 (예: 네팔)의 경우, 배경이 투명하지 않고 흰색 배경이 남습니다.
    PNG 형태로 변경해서 제공하는 것이 좋을 듯 싶더군요.
  • 이미지 해상도 : 저화질
    심지어 150x100 픽셀 같은 국기 이미지도 다수 보일 정도였습니다.
    국가 별 공식 국기 이미지인데... 흠... 이건 어디 아이콘으로 사용하려고 모아둔 느낌입니다.
    각 국가 별 공식 제공하는 이미지들을 모아서 올려둬도 이것보다는 나을 듯 싶은데... 이건 성의 문제 같습니다. 
  • 이미지 해상도 : 제각각
    국가 별 국기 이미지의 크기가 통일되지 않고 제각각입니다. 
    앞서의 150픽셀 수준보다 조금 더 큰 것도 있고 그렇지 않은 것들도 있고... 
    가로/세로 비율이 다른 국기들이 있으니 완전히 동일할 수는 없겠지만 그래도 가로 크기라도 맞춰주면 좋지 않을까 싶었습니다.
  • 설명 파일의 인코딩 : EUC-KR
    공공문서의 특징이긴 한데... 이제는 UTF-8로 바꿔서 제공하는 것이 필요할 것 같습니다. 
    그나마 아는 사람은, 지원되는 프로그램을 통해 인코딩을 바꿔서 열어보긴 할 것 같은데 
    그렇지 않은 사람들이나 외국인들의 경우는 완전히 파일이 깨진 것으로 오해하기 십상입니다. 

 

사진 : 다운받은 이미지 썸네일. 썸네일로 봐야 그럭저럭 괜찮습니다.

 

 

위 이미지들을 보면 썸네일이라 그럴 듯하게 보입니다만 각각 열면 해상도가 제각각입니다. -_-;

게다가 체크 표시된 NP.gif의 경우, 배경이 투명처리되지 못하니 하얀 배경으로 표시됩니다. 

그래서 다른 곳을 찾아보기로 하고 이 곳은 떠났습니다. (그래도 데이터 개선 요청은 해 두고 나왔습니다. ^^; )

 

그리고나서 찾아본 다양한 사이트들이 있습니다. 

  • Flags of the World (링크) : 국기와 관련된 퀴즈, 컬러링 등 다양한 서비스가 제공됩니다.
  • Country Flags (링크) : Vector, Image, icon 등 다양한 형태를 다운로드 가능
  • Wikimedia Commons의 Flags by country (링크) : 너무 복잡해요...  -_-.
  • FlagKit (링크) : 주로 아이콘 용 저해상도 이미지 제공
  • Flag Icons (링크) : github으로 clone해도 되고, 압축된 파일로도 제공됩니다. 

 

최종적으로 제가 선택한 곳은 Flags Icons 입니다.

SVG 파일이 잘 정리되어 있고 한번에 zip 파일로 다운받을 수도 있었습니다.

 

사진 : 국가 별 국기 svg 파일들 (다운받은 것 압축해제 후)

 

 

 

 

2.국가코드로 국가명 매칭하는 코드 작성

간단히 만들어봤습니다...라고 하려 했는데, 정작 국가코드를 가지고 있는 python 패키지가 없더군요.

찾아보니 pycountry이라는 녀석을 이용하면 국가코드(ISO 3166-1 alpha-2)와 영문 국가명을 가져올 수 있다고 합니다.

하지만, 한글로 된 국가명은 제공하지 않다보니 영문명을 Google Traslate 등을 이용해 다시 번역해야 하는 문제가 있었습니다. 

번역 성능이 워낙 좋아져서 큰 문제가 없겠다 싶긴 하지만 혹시나 그것보다는 다른 확실한 방법이 있을 것 같아 패스했습니다. 

 

그리고 나서 찾아본 것은 공공데이터포털(data.go.kr)의 한국국제협력단(KOICA) 국가코드 테이블이었습니다.

CSV 파일로 다운로드 받아서 보니 필요한 사항이 다 들어 있더군요. ^^; 그래서 이걸 쓰기로 했습니다.

국가명, 영문국가명, ISO-alpha2(국가코드 2글자), ISO-alpha-3(국가코드 3글자)만 남기고 없앴고 컬럼명도 변경했습니다.

 

 

 

그리고 이걸 불러서 dictionary로 만드는 코드를 준비했습니다.

 

import pandas as pd
import json

# CSV 파일을 읽어 딕셔너리 생성
file_path = 'cn_code_mapping.csv'
df = pd.read_csv(file_path)

# 결측값을 처리 (결측값이 있는 경우 빈 문자열로 대체)
df['cd_2c'] = df['cd_2c'].fillna('')

# country_dict 생성
country_dict = {
    row['cd_2c'].lower(): {
        'cn_kr': row['cn_kr'],
        'cn_en': row['cn_en'],
        'cd_2c': row['cd_2c']
    }
    for _, row in df.iterrows() if row['cd_2c']
}

# JSON 형식의 리스트 생성
json_list = [
    {
        "cd_2c": key,
        "cn_kr": value['cn_kr'],
        "cn_en": value['cn_en']
    }
    for key, value in country_dict.items()
]

# JSON 파일로 저장
output_file_path = 'country_data.json'
with open(output_file_path, 'w', encoding='utf-8') as json_file:
    json.dump(json_list, json_file, ensure_ascii=False, indent=4)

print(f"JSON 파일이 {output_file_path}에 저장되었습니다.")

 

 

country_data.json 파일은 다음과 같이 나옵니다. 

 

[
    {
        "cd_2c": "ga",
        "cn_kr": "가봉",
        "cn_en": "Gabon"
    },
    {
        "cd_2c": "gy",
        "cn_kr": "가이아나",
        "cn_en": "Guyana"
    },
    ...
]

 

 

3.퀴즈 프로그램 작성

 

이제 이 파일을 읽어서 국기 퀴즈를 낼 수 있도록 해 봅니다.

Javascript로 로컬 파일을 읽어 처리하는데 어려움이 있다고 하니 python 코드로 웹서버는 flask를 이용하는 방식을 택했습니다. 

 

생성할 파일, 폴더 구조는 다음과 같습니다. 

 

├── app.py
├── country_data.json
├── favicon.png
├── static
│   ├── countdown.mp3
│   ├── flags_data
│   └── script.js
└── templates
    └── index.html

 

 

app.py

flask를 이용하는 코드입니다.

ISO 2자리 국가코드를 이용하는데 혹시나 없는 경우를 걸러냅니다. (미리 수작업으로 확인하고 이 부분을 빼도 무방)

from flask import Flask, render_template, jsonify, send_from_directory
import json

app = Flask(__name__)

# JSON 파일 경로
JSON_FILE_PATH = 'country_data.json'

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/api/country-data')
def country_data():
    # JSON 파일 읽기
    with open(JSON_FILE_PATH, 'r', encoding='utf-8') as f:
        country_data = json.load(f)
    
    # ISO 2자리 코드를 소문자로 변환
    for item in country_data:
        if 'cd_2c' in item and item['cd_2c'] is not None:
            item['code'] = item.pop('cd_2c').lower()
        else:
            # cd_2c가 없거나 None인 경우 처리 (예: 기본값 할당 또는 로그)
            item['code'] = 'unknown'  # 예시로 'unknown' 코드 할당

    return jsonify(country_data)

if __name__ == '__main__':
    # 서버 시작
    app.run(host='0.0.0.0', port=5000)  # 포트 5000 사용

 

 

index.html

화면 구성을 하는 부분입니다.

  • 카운트다운 사운드를 지정했는데 파일 지정이 되면 그걸 이용하고 안 되어 있으면 코드 상에서 생성하게 됩니다. 

 

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>국가 퀴즈 프로그램</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #f0f0f0;
            margin: 0;
            overflow: hidden;
        }
        .quiz-container {
            text-align: center;
            background-color: white;
            padding: 20px;
            border: 1px solid #ccc;
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
            width: 80%; /* 너비 조정 */
            max-width: 600px; /* 최대 너비 설정 */
        }
        #flag-image {
            width: 100%; /* 이미지 크기를 부모 너비에 맞춤 */
            height: auto;
            margin-bottom: 20px;
            display: none; /* 초기에는 숨김 */
        }
        #countdown {
            font-size: 48px;
            font-weight: bold;
            color: #FF5733; /* 세련된 색상 */
            display: none; /* 초기에는 숨김 */
        }
        #result {
            font-size: 60px;
            margin-top: 20px;
            display: none;
        }
        #start-button {
            font-size: 24px;
            padding: 10px 20px;
            cursor: pointer;
            display: block; /* 초기에는 보임 */
            margin: 0 auto; /* 중앙 정렬 */
            background-color: #4CAF50; /* 버튼 색상 */
            color: white; /* 버튼 텍스트 색상 */
            border: none; /* 테두리 제거 */
            border-radius: 5px; /* 둥근 모서리 */
            transition: background-color 0.3s; /* 색상 전환 효과 */
        }
        #start-button:hover {
            background-color: #45a049; /* 호버 시 색상 변화 */
        }
        #welcome {
            font-size: 24px;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <div class="quiz-container">
        <div id="welcome">
            <h1>국가 퀴즈에 오신 것을 환영합니다!</h1>
            <p>각국의 국기를 보고, 해당 국가의 이름을 맞춰보세요!</p>
        </div>
        <button id="start-button" onclick="startGame()">게임 시작</button>
        <img id="flag-image" src="" alt="국기 이미지">
        <div id="countdown">3</div>
        <div id="result"></div>
    </div>

    <audio id="countdown-sound" preload="auto"></audio>
    <script src="static/script.js"></script>
</body>
</html>

 

 

script.js 파일 

  • 시작 부분에서 웰컴 멘트와 시작 버튼을 보이도록 하고 클릭하면 이후부터는 보이지 않도록 합니다. 
  • 문제가 시작되면 랜덤하게 이미지를 선택합니다. (중복 안 되도록 하는 부분을 추가로 넣어야 할 듯...)
  • 문제 별로 국기가 보여지면서 3초 카운트가 시작됩니다. -> 정확한 타이밍을 맞추지 못했습니다. 
let countryData = []; // 국가 데이터 저장 변수
let countdown = 3;
let currentCountry = null;
const countdownElement = document.getElementById('countdown');
const flagImage = document.getElementById('flag-image');
const resultElement = document.getElementById('result');
const welcomeElement = document.getElementById('welcome');

// 서버에서 국가 데이터 로드
fetch('/api/country-data')
    .then(response => response.json())
    .then(data => {
        countryData = data; // JSON 데이터를 배열에 저장
        document.getElementById('start-button').style.display = 'block'; // 버튼 표시
    })
    .catch(error => console.error('Error fetching country data:', error));

function startGame() {
    // 환영 멘트 숨기기
    welcomeElement.style.display = 'none';
    // 게임 시작 시 버튼을 숨깁니다.
    document.getElementById('start-button').style.display = 'none'; 
    startQuiz();
}

function startQuiz() {
    resultElement.style.display = 'none';
    countdownElement.style.display = 'none'; // 카운트다운 숨김 초기화

    // 랜덤한 국가 선택
    currentCountry = countryData[Math.floor(Math.random() * countryData.length)];

    // 국기 이미지 로드
    flagImage.src = `static/flags_data/${currentCountry.code}.svg`;
    flagImage.style.display = 'block';  // 이미지 표시

    // 1초 후에 카운트다운 시작
    setTimeout(() => {
        startCountdown();
    }, 1000);
}

function startCountdown() {
    countdownElement.style.display = 'block'; // 카운트다운 표시
    
    countdown = 3; // 카운트다운 초기화
    countdownElement.innerText = countdown;
    createSound(440, 0.2); // 음향 효과 생성
    countdown--;

    // 음향 효과 시작
    const countdownInterval = setInterval(() => {
        if (countdown >= 0) { // 카운트다운 숫자가 0 이상일 때만 소리 발생
            createSound(440, 0.2); // 음향 효과 생성
            countdownElement.innerText = countdown;
            countdown--;
        }

        if (countdown < 0) {
            clearInterval(countdownInterval);
            showAnswer();
        }
    }, 1000);
}

// 정답을 표시하는 함수
function showAnswer() {
    resultElement.innerText = `${currentCountry.cn_kr} (${currentCountry.cn_en})`;
    resultElement.style.display = 'block';
    countdownElement.style.display = 'none';

    // 1초 후 다음 문제로 자동으로 넘어가기
    setTimeout(() => {
        startQuiz();
    }, 1000);
}

// 소리 효과 생성 함수
function createSound(frequency, duration) {
    const audioContext = new (window.AudioContext || window.webkitAudioContext)();
    const oscillator = audioContext.createOscillator();
    oscillator.type = 'sine'; // 사인파
    oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
    oscillator.connect(audioContext.destination);
    oscillator.start();
    oscillator.stop(audioContext.currentTime + duration);
}

 

 

 

4.결과 테스트

 

프로그램 실행

 

아래와 같이 실행하면 브라우저에서 열 수 있는 링크가 나옵니다. (예: http://127.0.0.1:5000)

$ python app.py
 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://192.168.30.63:5000

 

 

첫 화면은 이렇게 나옵니다. 흠... 폰트 크기를 줄여서 한줄에 나오도록 해야겠지만 기능만 확인하는 것이니 패스~

 

 

게임이 시작되면 국기가 보이고 카운트가 시작됩니다. 

현재의 코드는 만들어진 기계음을 이용하는 것으로 되어 있습니다. 그리 듣기 좋진 않습니다... 

 

 

3초의 카운트가 끝나면 다음과 같이 국가의 이름이 표시됩니다. (위와 아래 이미지는 캡처 타이밍이 다름)

 

 

 

간단하게 아이와 함께 놀이를 할만한 게임을 만들어 봤습니다.

 

개선해야 할 부분은 다음과 같습니다.

  • 게임이 시작되면 전체 화면으로 전환 필요 (F11 효과)
  • 국기의 크기를 화면 크기에 맞춰 동적으로 변경
  • 제대로 된 카운트다운 음향효과 적용
  • 국기 별 난이도를 지정해서 초급, 중급, 고급 정도로 나눠서 문제 출제
  • 문제가 중복되지 않도록 처리하기 

위의 것 외에도 제대로 만들려면 손 봐야할 곳이 많네요. 

매번 그렇지만 이 정도 수준에서 기능 확인했으니 넘어가도록 합니다. ^^