[char RNN] 한국어 시 짓기 : 전처리 (시 입력 인터페이스)
char RNN은 분명 한계가 있어 보인다.
word RNN(단어 차원의 통제), sentence RNN(문장 수준의 통제), 맥락 알고리즘(줄거리, 기승전결 통제), sentimental 알고리즘(감성 분석 및 반영) 등의 알고리즘의 보완이 있어야 할 것으로 보인다.
셰익스피어 스타일 문장 생성 코드는 한국어 문장 생성에도 그대로 사용할 수 있다.
영어 알파벳이든 한국어 낱글자든 유니코드로 취급되기 때문에 가능한 일이다.
한국어 시는 대개 작가가 살아 있거나 작가가 별세한지 얼마 되지 않기 때문에 대부분 저작권이 살아 있다.
인터넷에 수록된 시들은 대개 저작권법 위반이지만, 작가와 출판사에서 별 문제를 제기하지 않아서 그냥 존치되는 듯하다.
특히 소설은 텍스트 파일로 함부로 퍼뜨리다간 저작권법 위반으로 고소될 수도 있다. 물론 시를 함부로 퍼뜨리는 것도 동일한 사안이 될 수 있다.
이 때문에 조금 걱정이 되었는데 과학 연구 목적으로 개인 차원에서 한국어 시들을 수집해서 활용하는 것은 관행상 이해될 수 있을 것 같아서 조심스레 진행해 보았다.
한국어 시들은 다음 사이트에서 주로 가져왔다.
'시 사랑' -> '없는 시 올리기' 게시판에 수록된 시들을 처음에는, 복사 붙여넣기 해서 텍스트 파일을 만들었는데,
시를 2천 개 정도 복사 붙여넣기를 하다가 중복 입력 문제가 발생하고, 편집 및 관리 문제도 있어서, 별도의 입력 프로그램을 제작해서 좀더 진지하게 입력하면서 시를 읽기도 하고, 감흥에 젖기도 하고, 띄어쓰기 수정하기도 하고(대개 원 시의 띄어쓰기가 문제가 아니라, 업로드 한 분이 실수한 것으로 보인다), 한자를 한글로 표기하기도 하면서 계속 입력하고 있다.
저작권 문제로 필자가 입력한 시들을 제공해드리기는 어렵고, 위의 사이트에서 시 데이터를 가져가서 활용하시길 바란다.
한 편의 시를 총 5개 필드로 구분했다. 필자의 시를 예로 들면...
--------------------
한동훈 --------> 작가 writer
정동진에서 ---------> 제목 title
- 시를 잃었다 ----------> 부제 subtitle
정동진에는 해가 없다 -------------> 본문 body
비는 있다
배는 산으로 가고
기차는 바다 속으로 들어간다
아이들은 포효하는 바다 속으로 뛰어들고
강은 산으로 흐르고
철조망은 바다를 덮치고
바다는 철조망*을 삼키고
모래시계는 거꾸로 흐른다
* 빌어먹을 철조망 ---------> 각주 footnote
--------------------
'시 입력 시스템 1.0' 외양
오른쪽 5개 입력창에 시를 입력하면 된다. 마지막 상태 칸은 상황을 알려주는 정보창이다. 장시간 입력하다 보니, 시력 보호를 어두운 배경을 택했다. 왼쪽 버턴들을 하나씩 설명한다.
그 전에 개략적인 작동 방식을 설명하면, GUI 인터페이스로 tkinter 를 사용하였고, 내부에서는 파이썬의 list 자료형을 사용한다. 외부 파일로의 저장은 pkl(피클) 기능을 사용하고, 최종적으로 머신러닝에 사용할 텍스트 파일을 생성할 수 있도록 했다.
기본적으로 복사 붙여넣기 기능이 되는 위젯들인지라, 오탈자 확인, 띄어쓰기 조정 등을 주로 하고 직접 타이핑하는 경우는 적다.
Open:
피클 파일을 불러온다. 처음에는 피클 파일이 없기 때문에, 시를 여러 편 입력하고 나서 'Save As...' 로 저장을 하면 피클 파일로 저장할 수 있다. 다음 작업 시에는 이것을 불러오면 된다.
Save As... :
입력한 내용들을 피클 파일로 저장.
Clear:
입력창에 있는 내용들을 모두 지운다. 기 입력된 데이터에는 지장을 주지 않음.
작가 검색:
작가명으로 검색한다. 검색 결과는 터미널(콘솔)에 나온다.(본 프로그램을 실행시킨 터미널)
가운데 있는 흰 작은 입력창을 공용 검색창으로 사용한다.
제목 검색:
제목으로 검색한다.
검색 결과는 터미널(콘솔)에 나온다.(본 프로그램을 실행시킨 터미널)
가운데 있는 흰 작은 입력창을 공용 검색창으로 사용한다.
인덱스 검색:
인덱스로 검색한다.
검색 결과는 터미널(콘솔)에 나온다.(본 프로그램을 실행시킨 터미널)
가운데 있는 흰 작은 입력창을 공용 검색창으로 사용한다.
검색 입력창:
작가명, 제목, 인덱스 등을 입력한다.
편집:
인덱스를 검색창에 입력하여 이 버턴을 클릭하면, 해당 시를 오른쪽 창에 불러온다.
삭제:
인덱스를 검색창에 입력하여 이 버턴을 클릭하면, 해당 시를 리스트에서 삭제할 수 있다. 삭제 의향 재확인함.
텍스트 파일 만들기:
머신러닝에 사용할 텍스트 파일 만드는 버턴.
소스 코드의 시작 부분에 있는 tmode 라는 변수값을 조정하여 무엇무엇을 출력할지 정할 수 있다.
tmode 값
0: 본문만, 1: 제목+부제+본문, 2: 제목+부제+작가+본문, 3: 제목+부제+작가+본문+각주
Add:
오른쪽에 입력한 시 정보를 리스트에 추가한다. 메모리에 기록되는 것이기 때문에 이따금 "Save As..."로 파일로 저장해주자.
#!/usr/bin/python3
# Programmed by 한동훈 (Don Han) 2021.7.19
# madrabbit7@naver.com blog.naver.com/madrabbit7
# 프로그램명: poem_input_sheet.py v1.2 시 입력 시스템
# 설명: 시를 입력하는 양식을 제공하여 피클(pkl) 파일로 저장한다.
# 내부적으로 리스트 자료형을 사용한다.
# 기능:
# - 입력 내용을 리스트에 넣어서, 필요시 피클 파일에 저장.
# - 입력 내용을 텍스트 파일로 출력. 출력 양식은 서너 가지 제공.
# - 추가, 검색, 수정, 삭제 등 편집 기능 제공
#-------------------- 시 예시
#
# 정동진에서 ---------> 제목 title
# - 시를 잃었다 ----------> 부제 subtitle
#
# 한동훈 ---------> 작가 writer
#
# 정동진에는 해가 없다 -------------> 본문 body
# 비는 있다
# 배는 산으로 가고
# 기차는 바다 속으로 들어간다
# 아이들은 포효하는 바다 속으로 뛰어들고
# 강은 산으로 흐르고
# 철조망은 바다를 덮치고
# 바다는 철조망*을 삼키고
# 모래시계는 거꾸로 흐른다
#
# *빌어먹을 철조망 ---------> 각주 footnote
import os
import random
import re
import pickle
import tkinter as tk
from tkinter.filedialog import askopenfilename, asksaveasfilename
import tkinter.messagebox as msgbox
# 시를 담은 리스트의 리스트, 전역 변수. 각 함수에서 조작한다.
# 본 프로그램을 실행하면 처음에는 빈 리스트로 초기화된다. 이후, 직접 하나씩 입력하든지,
# pkl 파일을 적재해서 사용한다.
poem_list = []
# 데이터를 수정할 때와 추가할 때는 구분해야 한다. 추가 작업 중일 때는 이 변수가
# -1 (비활성화)로 설정되어 있고, 수정 작업 중에는 수정하는 데이터의
# 인덱스값(0이상의 양수)으로 설정된다. 초기 설정은 데이터 추가 모드이다.
modify_index = -1
# 텍스트 파일로 출력할 때 무엇무엇을 출력할지 정하는 변수
# 0: 본문만, 1: 제목+부제+본문, 2: 제목+부제+작가+본문, 3: 제목+부제+작가+본문+각주
tmode = 0
# 작가명 글자수 (2자, 3자, 4자 가능)
writer_length = 3
# 리스트 데이터를 텍스트 파일로 출력하는 함수. 서너 가지 출력 옵션을 제공한다.
# 텍스트 파일 내부 줄 띄우기:
# - 시와 시 사이에는 두 줄을 띄운다.
# - 한편의 시 안에서는 최대 한 줄을 띄운다.
# - 제목과 부제, 작가명 사이는 띄우지 않는다.
# - 본문 위에 두 줄을 띄운다.
def list_to_textfile():
# 기존 파일을 덮어쓰지 않도록 난수를 발생시켜 파일명에 포함한다.
num = str(random.randint(1000, 10000)) # 파일명에 네 자리 숫자 포함
text_filename = f"korean_poems_{num}.txt"
global poem_list
global tmode
# 메시지 박스를 통해 '확인'이면 1, '취소'면 0을 response로 얻는다.
response = msgbox.askokcancel("확인/취소", f"{text_filename}을 생성합니다. 진행하시겠습니까?")
if response == 0: # 취소면 그냥 리턴함(작업 취소)
print("텍스트 파일 만들기를 취소합니다.")
return
# response가 1이면 텍스트 파일을 만든다.
f = open(text_filename, "w")
# 리스트에 있는 시를 한 편씩 가져온다.
for poem in poem_list:
# 제목 출력
if tmode != 0: # 제목이 있다면
f.write(poem[1]) # 제목 출력
f.write("\n")
if poem[2] != "": # 부제가 있다면
f.write("-")
f.write(poem[2]) # 부제 출력
f.write("\n")
# 작가명이 있다면
if tmode == 2 or tmode == 3:
f.write(poem[0]) # 작가명 출력
f.write("\n")
if tmode != 0:
f.write("\n\n") # 제목/작가 다음 두 줄 띄우고
#---- 본문 출력 : 데이터 전처리 과정
# 머신러닝 훈련에 사용될 핵심 데이터이므로 필요한 전처리를 진행한다.
body = poem[3].replace(u"\u200b", "") # 폭없는 공백 문자 제거
body = body.replace(u"\u3000", "") # 특수 문자 제거
body = re.sub("[‘’“”]", "'", body) # 인용문자를 ' 로 통일
body = re.sub("\.\.\.", "…", body) # 비표준 말줄임표를 표준 말줄임표 하나로.
body = re.sub("……", "…", body) # 말줄임표 2개를 1개로.
body = body.replace("1월", "일월") # 시에서 숫자는 가급적 피한다. 숫자 다음에 나오는 한글이 딱 정해져버리니까.
body = body.replace("2월", "이월")
body = body.replace("3월", "삼월")
body = body.replace("4월", "사월")
body = body.replace("5월", "오월")
body = body.replace("6월", "유월")
body = body.replace("7월", "칠월")
body = body.replace("8월", "팔월")
body = body.replace("9월", "구월")
body = body.replace("10월", "십월")
body = body.replace("11월", "십일월")
body = body.replace("12월", "십이월")
f.write(body)
f.write("\n\n")
# 각주 있으면 출력
if tmode == 3:
if poem[4] != "":
f.write(poem[4])
f.write("\n\n")
f.write("-----------\n")
f.write("\n")
f.close()
print(f"{text_filename}을 섹시하게 만들었습니다.")
# 피클 파일을 열어서 리스트로 적재하는 함수.
def open_file():
# 피클 파일을 선택하도록 한다.
filepath = askopenfilename(
filetypes=[("Pkl Files", "*.pkl")])
global poem_list
# 선택 취소하였거나 선택된 파일명이 파일이 아니라면 작업 거부
if not filepath or os.path.isfile(filepath) == False:
return
else: # 파일이 있을 경우 열어서 리스트로 받음
with open(filepath, 'rb') as f:
poem_list = pickle.load(f)
# 상태 칸에 메시지 출력
status_var.set("")
data_len = len(poem_list)
string = str(data_len) + "개의 데이터를 불러왔습니다."
status_var.set(string)
# 적재한 파일명을 메인창 이마빡에 표시
root.title(f"시 입력 시스템 - {filepath}")
# 리스트의 내용을 피클 파일로 저장
def save_file():
global poem_list
# 저장할 피클 파일의 이름을 선택하도록 한다.
filepath = asksaveasfilename(
defaultextension=".pkl",
filetypes=[("Pkl Files", "*.pkl")], )
if not filepath: # 취소하였다면 작업 안 함
return
# 존재하는 파일이라면 덮어쓰고, 아니라면 해당 파일을 만든다.
with open(filepath, 'wb') as f:
pickle.dump(poem_list, f)
# 상태 칸에 메시지 출력
status_var.set("")
data_len = len(poem_list)
string = str(data_len) + f"개의 데이터를 {filepath} 로 저장했습니다."
status_var.set(string)
# 출력 완료한 피클 파일명을 메인창 이마빡에 표시
root.title(f"시 입력 시스템 - {filepath}")
# 작가명 글자수를 조정하는 부분. 글자수는 2자나 3자, 4자 중 하나
# 이렇게 정하는 이유는, 많은 시를 입력하다 보면 작가명과 제목을 뒤바꿔
# 입력할 수 있는데, 이를 입력 단계에서 방지하기 위해서다.
def change_writer_length():
global writer_length
if writer_length == 3:
writer_length = 4
elif writer_length == 4:
writer_length = 2
elif writer_length == 2:
writer_length = 3
else:
writer_length = 3
status_var.set("")
string = f"작가명를 {writer_length}자로 설정합니다."
status_var.set(string)
# 시를 작가명으로 검색
# search.var 검색창 인스턴스(변수)는 "작가 검색/제목 검색/인덱스 검색/편집/삭제" 에서 공동으로 사용한다.
def search_writer():
# 입력 내용이 없다면
if search_var.get().strip() == "":
print("검색 작가명을 입력해주세요.")
return
found_index_list = []
found_title_list = []
#시를 한 편씩 불러와서 검색 작가명과 대조한다
for i, poem in enumerate(poem_list):
if poem[0].strip() == search_var.get().strip(): # 동일하면
found_index_list.append(i) # 해당 시의 인덱스를 기록
found_title_list.append(poem[1]) # 해당 시의 제목을 기록
# 콘솔에 검색 결과를 출력한다
print(found_index_list)
print(found_title_list)
print(len(found_index_list), "개를 찾았습니다.")
# 인덱스로 시를 검색
def search_index():
# search_var 전역 변수(인스턴스) 사용
# 빈 문자열일 경우
if search_var.get().strip() == "":
print("검색할 인덱스를 입력해주세요.")
return
# 숫자가 아닌 경우, 정수로 변환하면 에러가 발생한다.
try:
index = int(search_var.get().strip())
except:
print("숫자를 입력해주세요.")
return
# 범위를 벗어난 숫자일 경우, 경고하고 리턴
if index >= len(poem_list) or index < 0:
print(len(poem_list) - 1, "이하의 인덱스여야 합니다.")
return
# 검색 결과를 콘솔에 출력
print("Index: ", str(index))
print(poem_list[index])
# 제목으로 시 검색
def search_title():
# 빈 문자열일 경우
if search_var.get().strip() == "":
print("검색할 제목을 입력해주세요.")
return
found_list = []
# 리스트에서 시를 한 편씩 가져와서 검색 제목과 대조한다.
# 양쪽 제목에서 모든 공백을 제거하고 비교한다.
for i, poem in enumerate(poem_list):
if poem[1].strip().replace(" ", "") == search_var.get().strip().replace(" ", ""):
found_list.append(i) # 찾았다면 해당 인덱스 기록
# 검색 결과를 콘솔에 출력
print(found_list)
def search_body():
# 빈 문자열일 경우
if search_var.get().strip() == "":
print("검색할 본문을 입력해주세요.")
return
found_list = []
# 리스트에서 시를 한 편씩 가져와서 검색 본문과 대조한다.
# 양쪽 본문에서 모든 공백과 개행문자를 제거하고 비교한다.
source = search_var.get().strip().replace(" ", "").replace("\n", "")
for i, poem in enumerate(poem_list):
if source in poem[3].strip().replace(" ", "").replace("\n", ""):
found_list.append(i) # 찾았다면 해당 인덱스 기록
# 검색 결과를 콘솔에 출력
print(found_list)
# 인덱스로 시를 찾아 제거하기
def delete_index():
# 전역 변수 search_var 사용
# 검색창 내용이 비었다면
if search_var.get().strip() == "":
msgbox.showerror("오류", "삭제할 인덱스를 입력해주세요")
return
# 숫자가 아닌 문자를 입력했을 경우, 그냥 리턴
# 일반 문자일 경우, int 형변환을 시도하면 에러가 남.
try:
index = int(search_var.get().strip())
except:
msgbox.showerror("오류", "숫자를 입력해주세요.")
return
# 범위를 벗어난 숫자일 경우, 경고하고 리턴
if index >= len(poem_list) or index < 0:
len_poems = len(poem_list)
string = f"{len_poems} 이내의 인덱스여야 합니다."
msgbox.showerror("오류", string)
return
# 삭제하기 전에 확인한다.
response = msgbox.askokcancel("확인/취소", f"{index}번 인덱스를 정말 삭제하시겠습니까?")
if response == 0: # 취소
print("삭제를 취소합니다.")
return
# response가 1이라면 삭제 진행
# 콘솔에 진행 상태 출력
print(index, "번 데이터를 삭제합니다.")
print(poem_list[index])
del poem_list[index] # 삭제
print("현재 ", len(poem_list), "개 데이터가 있습니다.")
# 입력한 데이터가 기존 리스트에 있는지 검사한다.
# 작가와 제목이 같으면 중복 데이터로 간주.
# 중복이면 (True, 중복 데이터 인덱스)를 반환. 못 찾았으면 (False, -1) 반환
def is_duplicate(data):
global poem_list
found_index = -1 # 못 찾았을 때의 인덱스를 일단 표식
found = False # 못 찾았다고 일단 표식
if len(poem_list) == 0: # (메인)리스트에 시가 한 편도 없을 경우에는 그냥 리턴
return found, found_index
# (메인)리스트에서 시를 한편씩 가져와서 삭제 작가명과 제목에 대조한다
for i, poem in enumerate(poem_list):
# (메인)리스트에 저장할 때 이미 strip()을 했으므로,
# 제목의 경우, 단어 사이의 공백을 양쪽 공히 제거하고 비교한다.
# 작가명이 같고, 제목이 같다면
if data[0] == poem[0] and data[1].replace(" ", "") == poem[1].replace(" ", ""):
found_index = i # 해당 인덱스를 기록
found = True # 찾았음을 표시
break # 루프를 탈출한다
return found, found_index # (찾았음 표시, 찾은 인덱스)를 반환
# 시 입력 창을 클리어한다 (상태 칸 제외)
def clear_text_input():
writer_var.set("")
title_var.set("")
subtitle_var.set("")
body_text.delete(1.0, tk.END)
footnote_text.delete(1.0, tk.END)
# Clear 버턴 눌렀을 시, 입력창 클리어 및 설정
def clear_text():
global modify_index
# 수정하다가 취소하는 것일 수도 있으므로, (기본 모드인) 추가 모드로 전환한다
modify_index = -1
clear_text_input()
status_var.set("")
search_var.set("")
# 인덱스로 데이터를 불러와서 수정한 뒤, 기존 데이터 위에 덮어쓴다.
# 검색창에 인덱스 번호를 입력하고 '편집' 버턴을 누르면,
# 해당 데이터를 가져와서 시 입력 창에 뿌려준다.
# 수정 후, Add 버턴을 눌러 기존 데이터를 덮어쓰면 된다.
def poem_edit():
global modify_index
# 검색창이 비어 있다면
if search_var.get().strip() == "":
print("수정할 데이터의 인덱스를 입력해주세요.")
return
# 숫자가 아닌 문자일 경우, 그냥 리턴
try:
index = int(search_var.get().strip())
except:
print("숫자를 입력해주세요.")
return
# 범위를 벗어난 숫자일 경우, 경고하고 리턴
if index >= len(poem_list) or index < 0:
print(len(poem_list) - 1, "이하의 인덱스여야 합니다.")
return
# 데이터를 불러와서 시 입력창에 뿌려준다.
writer_var.set(poem_list[index][0])
title_var.set(poem_list[index][1])
subtitle_var.set(poem_list[index][2])
body_text.insert(tk.END, poem_list[index][3])
footnote_text.insert(tk.END, poem_list[index][4])
# 진행 상황을 상태 칸에 출력한다.
status_var.set(f"{index}번 데이터를 불러왔습니다.")
# modify_index 에 수정 중인 인덱스 숫자를 넣는다. 이제 수정모드인 것이다.
modify_index = index
# 시 입력창의 데이터를 리스트에 추가한다.
def add_poem():
global poem_list
global modify_index
global writer_length
# 작가명, 제목, 부제, 본문, 각주 총 5개 문자열을 가져온다.
writer = writer_var.get()
title = title_var.get()
subtitle = subtitle_var.get()
body = body_text.get("1.0", tk.END)
# 본문의 경우, 리스트에 추가하기 전에 간단한 전처리를 한다.
body = body.replace(u"\u200b", "") # '폭없는 공백 문자' 제거
body = body.replace(u"\u3000", "") # 특수 문자 제거
footnote = footnote_text.get("1.0", tk.END)
# 리스트 1개(시 한편)를 만들어서 메인 리스트(poem_list)에 넣을 준비를 한다.
# 각 항목별로, 앞뒤에 따르는 공백문자, 탭, 개행문자 등을 strip()으로 제거한다.
poem = [writer.strip(), title.strip(), subtitle.strip(), body.strip(), footnote.strip()]
# 작가명이 지정한 글자수(2~4자)가 아니라면 작업 거부.
# 입력 실수 방지를 위해 작가명을 2~4자 중 한 가지로 제한.
if len(writer.strip()) != writer_length:
status_var.set("")
string = f"작가명이 {writer_length}자가 아닙니다."
status_var.set(string)
return
# 작가명에 공백이 포함되어 있으면 작업 거부
if writer.strip().find(" ") != -1: # 공백을 찾았을 경우
status_var.set("")
string = "작가명에 공백이 있습니다."
status_var.set(string)
return
# 제목이 없거나, 본문이 없으면 작업 거부
if title.strip() == "" or body.strip() == "":
status_var.set("")
string = "제목이 없거나 본문이 없습니다."
status_var.set(string)
return
# 데이터 수정 모드
if modify_index >= 0: # 즉 인덱스 숫자가 -1이 아니라면
poem_list[modify_index] = poem # 해당 인덱스의 데이터에 변경된 데이터 덮어쓰기
print(poem_list[modify_index]) # 갱신된 데이터를 콘솔에 출력
# 시 입력창을 클리어한다
status_var.set("")
string = f"{modify_index}인덱스 데이터를 변경했습니다."
status_var.set(string) # 상태 칸에 상태 출력
clear_text_input()
modify_index = -1
return
# 추가모드일 경우.
# 중복 데이터가 있는지 검사한다.
found, found_index = is_duplicate(poem)
# 중복 데이터가 있다면 경고 메시지 출력하고 리턴.
if found == True:
status_var.set("")
string = "기존 " + str(found_index) + "번 인덱스와 데이터가 중복됩니다."
status_var.set(string)
return
else: # 중복 데이터가 없다면
poem_list.append(poem) # 리스트에 해당 시를 추가
# 상태 칸에 메시지 추가
status_var.set("")
data_index = len(poem_list) - 1
string = str(data_index) + "번 인덱스를 추가했습니다."
status_var.set(string)
# 리스트에 추가된 시를 콘솔에 출력
print(poem_list[len(poem_list) - 1])
# 입력 양식 클리어
clear_text_input()
modify_index = -1
#---------------------------------------------------------------------------
# 좌우로 프레임을 나누고, 좌측에 버턴을 배열하는 구성은 아래 코드를 참고했다.
# https://www.studytonight.com/tkinter/text-editor-application-using-tkinter
root = tk.Tk()
root.title("시 입력 시스템")
# 전체 열은 1개, 컬럼은 2개
root.rowconfigure(0, minsize=800, weight=1)
root.columnconfigure(1, minsize=800, weight=1)
# 입력창들을 위한 프레임을 생성
text_edit = tk.Frame(root)
# 인스턴스 변수를 생성
writer_var = tk.StringVar()
title_var = tk.StringVar()
subtitle_var = tk.StringVar()
body_var = tk.StringVar()
footnote_var = tk.StringVar()
# 상태 출력에 이용되는 변수
status_var = tk.StringVar()
# search_var 변수는 여러 곳에서 함께 사용
search_var = tk.StringVar()
# 텍스트 입력창 색상
fgc = "gray80" # 글자색. 밝은 회색. 숫자가 높을수록 밝음
bgc = "gray20" # 배경색. 어두운 회색.
# 버턴들을 위한 프레임 생성
fr_buttons = tk.Frame(root, relief=tk.RAISED, bd=2)
# 각종 버턴들과 검색창 설정, 연결 함수도 지정
btn_open = tk.Button(fr_buttons, text="Open", command=open_file)
btn_save = tk.Button(fr_buttons, text="Save As...", command=save_file)
btn_clear = tk.Button(fr_buttons, text="Clear", command=clear_text)
btn_writer_length = tk.Button(fr_buttons, text="작가명 글자수", command=change_writer_length)
btn_search_writer = tk.Button(fr_buttons, text="작가 검색", command=search_writer)
btn_search_title = tk.Button(fr_buttons, text="제목 검색", command=search_title)
btn_search_body = tk.Button(fr_buttons, text="본문 검색", command=search_body)
btn_search_index = tk.Button(fr_buttons, text="인덱스 검색", command=search_index)
# 이것 하나만 검색용 입력창임.
search_entry = tk.Entry(fr_buttons, textvariable=search_var, font=('calibre', 13))
btn_poem_edit = tk.Button(fr_buttons, text="편집", command=poem_edit)
btn_delete_index = tk.Button(fr_buttons, text="삭제", command=delete_index)
btn_list_to_textfile = tk.Button(fr_buttons, text="텍스트 파일 만들기", command=list_to_textfile)
btn_add = tk.Button(fr_buttons, text="Add", command=add_poem)
# 버튼들과 검색창의 위치와 형태를 잡아줌
btn_open.grid(row=0, column=0, sticky='ew', padx=4, pady=4)
btn_save.grid(row=1, column=0, sticky='ew', padx=4, pady=4)
btn_clear.grid(row=2, column=0, sticky='ew', padx=4, pady=4)
btn_writer_length.grid(row=3, column=0, sticky='ew', padx=4, pady=4)
btn_search_writer.grid(row=4, column=0, sticky='ew', padx=4, pady=4)
btn_search_title.grid(row=5, column=0, sticky='ew', padx=4, pady=4)
btn_search_body.grid(row=6, column=0, sticky='ew', padx=4, pady=4)
btn_search_index.grid(row=7, column=0, sticky='ew', padx=4, pady=4)
search_entry.grid(row=8, column=0, sticky='ew', padx=4, pady=4)
btn_poem_edit.grid(row=9, column=0, sticky='ew', padx=4, pady=4)
btn_delete_index.grid(row=10, column=0, sticky='ew', padx=4, pady=4)
btn_list_to_textfile.grid(row=11, column=0, sticky='ew', padx=4, pady=4)
# pady값을 크게 줘서 위 버튼과의 거리를 넓게 벌림
btn_add.grid(row=12, column=0, sticky='ew', padx=4, pady=100)
# 버턴들을 거느린 프레임을 왼쪽에 둠
fr_buttons.grid(row=0, column=0, sticky='ns')
# 오른쪽의 입력창들을 설정함
writer_label = tk.Label(text_edit, text='작가', font=('calibre', 10))
writer_entry = tk.Entry(text_edit, width=100, textvariable=writer_var, bg=bgc, fg=fgc, font=('calibre', 13))
# 배경을 어둡게 한 탓으로 cursor 색깔을 밝게.
writer_entry.configure(insertbackground='white')
title_label = tk.Label(text_edit, text='제목', font=('calibre', 10))
title_entry = tk.Entry(text_edit, width=100, textvariable=title_var, bg=bgc, fg=fgc, font=('calibre', 13))
title_entry.configure(insertbackground='white')
subtitle_label = tk.Label(text_edit, text='부제', font=('calibre', 10))
subtitle_entry = tk.Entry(text_edit, width=100, textvariable=subtitle_var, bg=bgc, fg=fgc, font=('calibre', 13))
subtitle_entry.configure(insertbackground='white')
body_label = tk.Label(text_edit, text="본문", font=('calibre', 10))
body_text = tk.Text(text_edit, width=100, height=29, font=('calibre', 13), bg=bgc, fg=fgc, selectbackground="yellow", selectforeground='black')
body_text.configure(insertbackground='white')
footnote_label = tk.Label(text_edit, text="각주", font=('calibre', 10))
footnote_text = tk.Text(text_edit, width=100, height=2, bg=bgc, fg=fgc, font=('calibre', 13))
footnote_text.configure(insertbackground='white')
status_label = tk.Label(text_edit, text="상태", font=('calibre', 10))
status_entry = tk.Entry(text_edit, width=100, textvariable=status_var, bg=bgc, fg=fgc, font=('calibre', 13))
status_entry.configure(insertbackground='white')
# 입력창들의 위치와 외양 설정
writer_label.grid(row=0, column=0, padx=4, pady=4)
writer_entry.grid(row=0, column=1, padx=4, pady=4)
title_label.grid(row=1, column=0, padx=4, pady=4)
title_entry.grid(row=1, column=1, padx=4, pady=4)
subtitle_label.grid(row=2, column=0, padx=4, pady=4)
subtitle_entry.grid(row=2, column=1, padx=4, pady=4)
body_label.grid(row=3, column=0, padx=4, pady=4)
body_text.grid(row=3, column=1, padx=4, pady=4)
footnote_label.grid(row=4, column=0, padx=4, pady=4)
footnote_text.grid(row=4, column=1, padx=4, pady=4)
# 상태를 출력하는 상태 칸은 입력창 형태지만 출력용으로만 사용됨.
status_label.grid(row=5, column=0, padx=4, pady=8)
status_entry.grid(row=5, column=1, padx=4, pady=8)
# 입력창들을 거느린 텍스트 프레임을 오른쪽에 위치시킴
text_edit.grid(row=0, column=1, sticky='news')
root.mainloop()