본문 바로가기

내 이야기/프로젝트

주소 인식과 날짜 인식을 위한 삽질의 기록

개요

2021.01.02 ~ 2021.01.05 혼자

 

당근마켓 거래 채팅방 중에 주소를 입력하면 이를 인식하여 안내 알람을 보내주는 기능이 있었습니다. 당근마켓 팀 블로그 내 해당 기능과 관련된 구현 이야기를 보니 흥미로웠습니다. 그래서 저도 따라 만들어보고자 했습니다.

 

주소 인식을 위한 삽질의 기록

당신의 안전을 위해서라면 뭐든지 할 거예요. 그게 삽질이라도…

medium.com

 그리고 주소 인식 뿐만 아니라 약속 날짜나 시간을 이야기하면 해당 데이터도 인식하여 재밌는 기능들도 넣어볼 수 있지 않을까 생각했습니다.

 

 여담으로 저의 일화를 소개해드리자면... 평일 저녁 8시에 갤럭시 버즈 기기를 판매하기로 약속을 했었습니다. 그런데 약속 당일에 정신없이 하루를 보내다보니 시간이 임박해왔던 걸 몰랐습니다. 그러다 약속 시간 30분 전, 거래자 분께서 오고 계시냐는 묻는 말에 뒤늦게 약속을 미루고 가격을 깎아드렸던 일화가 있습니다. 

 

 물론, 휴대폰 알람이 위 상황에서 가장 효과적인 방법이었겠지만 채팅봇이 양쪽 모두에게 채팅으로 알람을 보내주면 어떨지. 애플리케이션 내에서 자동으로 처리해주면 재밌지 않을까 생각했었습니다. 즉, 기능적으로 유용해서라기보다는 이런 기능도 있으면 재밌지 않을까하는 호기심으로 구현해봤습니다. 실제 완성도도 생각보다 만족스럽지 못했습니다ㅠ.

 


설계 및 구현

주소 인식 기능

약속 장소 인식

가정

  • 도로명 주소 및 지번 주소를 약속 장소(주소)로 인식한다

 


구현 

  • (당근마켓 블로그 글을 보며 그대로 구현하였습니다)
  • 도로명 주소, 지번 주소 인식 정규식 검사
  • 개발자센터에서 제공중인 파일로 DB 구성해 쿼리 검색(해당 주소 존재 여부)

 


문제점 및 추후 해결 과제

  • 띄어쓰기를 제대로 하지 않은 주소 텍스트에 대한 처리 (ex. “구로구디지털로30길28”)
  • 동호수가 숫자가 아닌 문자열인 경우에 대한 처리 (ex. A동101호)
  • 사무실이나 주택 등에 대한 다양한 주소 처리 (ex. 건물 이름과 층수로 끝나는 주소 또는 다세대 주택의 주소 형식 등 — 마리오타워 609호)
  • 자양동 8시에 볼까요 > 자양동 8까지 주소로 인식
  • 지하철역 출구 주소 인식
  • 성능 문제 DB 인덱스(한글 데이터...), Redis 

 

날짜 인식 기능

 

약속 날짜 인식

 

약속 시간 인식



가정

  • 채팅 하나당 하나의 거래 > 하나의 약속 가정 > 하나의 알람 가정!
  • 약속은 미래를 일컫는 말이므로 인식한 날짜는 무조건 미래 날짜 데이터로 인식
  • ex) 오늘이 12월 30일 경우, "1월 4일 어때요?" => 내년 1월 4일로 인식

 


구현

  • 날짜 인식 > 한글 일자 인식(내일, 모레 등), 요일 인식(월욜 등), 주 + 요일 인식(담주 월욜 등), 한글 날짜 인식(4월 12일), 숫자 슬래쉬 날짜 인식(12/30)
  • 시간 인식 > 숫자 + 한글 검색(4시 30분), 숫자:숫자(04:30)
  • 정규식을 통해 파싱 후 날짜 및 시간 데이터로 변환하여 DB에 저장한다.
  • 그리고 사용자가 알림을 원할 경우 위의 정보를 통해 알림을 생성해서 푸시 등의 형태로 알림을 보낸다(실제 발송 X)

 

 

 

 

문제점 및 추후 해결 과제

  • 기능에 비해 과도한 연산
  • 1/2 분수 표현에 대한 처리
  • 오전/오후 구분
  • 한글 시간 인식(한시, 두시, 2시 반 등) 

 

데이터베이스

 

 

주소 인식 소스코드

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
    # 주소검색 <1>
    # 도로명 주소 검색 
    def street_address_check(str)
      addr_str = str.match(/(([가-힣A-Za-z·\d~\-\.]{2,}(로|길).[\d]+))/)
      street_str = addr_str.to_s().split(' ')[0]
      # 도로명 검색
      if !street_str.nil?
        # 테이블 내 검색 있으면 데이터 반환
        if Address.where("street = ?", street_str).count != 0
          return addr_str
        end
      end
 
      # 도로명 주소 아님 false
      return false
    end
 
    # 주소검색 <2>
    # 지번 주소 검색 
    def lot_address_check(str)
      addr_str = str.match(/(([가-힣A-Za-z·\d~\-\.]+(읍|동)\s)[\d-]+)|(([가-힣A-Za-z·\d~\-\.]+(읍|동)\s)[\d][^시]+)/)
      dong_str = addr_str.to_s().split(' ')[0]
      # 지번주소 검색
      if !dong_str.nil?
        # 테이블 내 검색 있으면 데이터 반환
        if Address.where("dong = ?", dong_str).count != 0
          return addr_str
        end
      end
 
      # 지번 주소 아님 false
      return false
    end
cs

 

날짜 및 시간 인식 소스코드 

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
     # 날짜검색 <1>
    # 약속 날짜 체크
    def korean_date_check(str)
 
      # 며칠 뒤의 날짜인지 체크
      plus_date = nil
      if str.match(/(오늘)|(금일)/)
        plus_date = 0
      elsif str.match(/(내일)|(차일)/)
        plus_date = 1
      elsif str.match(/(모레)|(이틀)/
        plus_date = 2
      elsif str.match(/(사흘)/)
        plus_date = 3
      elsif str.match(/(나흘)/)
        plus_date = 4 
      elsif str.match(/(닷새)/)
        plus_date = 5
      end
      
      if plus_date
        return Date.today.to_datetime + plus_date.days
      else
        return nil
      end
    end
 
 
    # 날짜검색 <2>
    # 약속 날짜 요일 체크
    def wday_check(str)
      
      plus_week = nil
      when_wday = nil
 
      # 이번주 요일 검색
      if str.match(/(이번|이번 |금)[주]\s[월](요일||)/
        when_wday = 0
        plus_week = 0
      elsif str.match(/(이번|이번 |금)[주]\s[화](요일||)/
        when_wday = 1
        plus_week = 0 
      elsif str.match(/(이번|이번 |금)[주]\s[수](요일||)/
        when_wday = 2
        plus_week = 0
      elsif str.match(/(이번|이번 |금)[주]\s[목](요일||)/
        when_wday = 3
        plus_week = 0
      elsif str.match(/(이번|이번 |금)[주]\s[금](요일||)/
        when_wday = 4
        plus_week = 0 
      elsif str.match(/(이번|이번 |금)[주]\s[토](요일||)/)
        when_wday = 5
        plus_week = 0
      elsif str.match(/(이번|이번 |금)[주]\s[일](요일||)/
        when_wday = 6
        plus_week = 0
      end
 
      # 다음주 요일 검색
      if str.match(/(다음|다음 ||담 |차)[주]\s[월](요일||)/
        when_wday = 0
        plus_week = 1
      elsif str.match(/(다음|다음 ||담 |차)[주]\s[화](요일||)/
        when_wday = 1
        plus_week = 1
      elsif str.match(/(다음|다음 ||담 |차)[주]\s[수](요일||)/
        when_wday = 2
        plus_week = 1
      elsif str.match(/(다음|다음 ||담 |차)[주]\s[목](요일||)/
        when_wday = 3
        plus_week = 1
      elsif str.match(/(다음|다음 ||담 |차)[주]\s[금](요일||)/
        when_wday = 4
        plus_week = 1 
      elsif str.match(/(다음|다음 ||담 |차)[주]\s[토](요일||)/
        when_wday = 5
        plus_week = 1
      elsif str.match(/(다음|다음 ||담 |차)[주]\s[일](요일||)/
        when_wday = 6
        plus_week = 1
      end
 
      # <주 + 요일> 파싱 안되면 단순 <요일> 파싱 
      if plus_week.nil&& when_wday.nil?
        
        if str.match(/[월](요일|욜)/
          when_wday = 0
        elsif str.match(/[화](요일|욜)/
          when_wday = 1
        elsif str.match(/[수](요일|욜)/
          when_wday = 2
        elsif str.match(/[목](요일|욜)/
          when_wday = 3
        elsif str.match(/[금](요일|욜)/
          when_wday = 4
        elsif str.match(/[토](요일|욜)/
          when_wday = 5
        elsif str.match(/[일](요일|욜)/
          when_wday = 6
        end
 
        # 단순 요일 파싱도 안되면 처리
        if when_wday.nil
          return nil
 
        # 단순 요일 파싱되면 처리
        else
          # 일 0 ~ 토 6 (변환)=> 월 0 ~ 일 6
          now_wday = (Date.today.to_datetime.wday-1) % 7
          
          if when_wday > now_wday
            plus_wday = (when_wday - now_wday) 
          
          else
            plus_wday = (when_wday + 7- now_wday
          
          end
          # 변환 날짜 계산 후 반환
          return Date.today.to_datetime + plus_wday.days
        
        end
 
      # 파싱되면 날짜 차이 계산
      else
        # 일 0 ~ 토 6 (변환)=> 월 0 ~ 일 6
        now_wday = (Date.today.to_datetime.wday-1) % 7
        plus_wday = when_wday - now_wday
 
        # 변환 날짜 계산 후 반환
        return Date.today.to_datetime + (plus_wday + (plus_week)*7).days
      end
    end
 
    # 날짜검색 <3>
    # 약속 날짜 체크 
    def date_check(str)
      slash_date_str = str.match(/[0-1]?[0-9]\/[0-3]?[0-9]/)
      korean_date_str = str.match(/([0-1]?[0-9]월\s[0-3]?[0-9]일)|([0-1]?[0-9]월[0-3]?[0-9]일)/)
      ret_date = nil
 
      # 00/00
      if slash_date_str
        # 내년을 말한 것인지 체크 
        slash_date = slash_date_str.to_s.to_date.
        if slash_date.change(year:0< Date.today.change(year:0)
          ret_date = slash_date + 1.year
        else
          ret_date = slash_date
        end
 
      # 00월 00일
      if korean_date_str
        str_list = korean_date_str.to_s.split('월')
        # 내년을 말한 것인지 체크 
        korean_date = (str_list[0]+"/"+str_list[1]).to_date
        if korean_date.change(year:0< Date.today.change(year:0)
          ret_date = korean_date + 1.year
        else
          ret_date =korean_date
        end
      end
      
      return ret_date
    end
 
    # 날짜검색 <4>
    # 약속 시간 체크
    def time_check(str)
      hour_minute_str = str.match(/([0-2]?[0-9]시[0-5][0-9]분)|([0-2]?[0-9]시\s[0-5]?[0-9]분)|([0-2]?[0-9]시)/)
      colon_time_str = str.match(/([0-2]?[0-9]:[0-5]?[0-9])/)
      
      # 00시 00분 , 00시00분, 00시
      if hour_minute_str
        str_list = hour_minute_str.to_s.split('시')
        if str_list.size == 1 
          return Time.new.change(hour:(str_list[0].to_i + 12))
        else 
          return Time.new.change(hour:(str_list[0].to_i + 12), min:str_list[1].to_i)
        end
 
      # 00:00
      elsif colon_time_str 
        str_list = colon_time_str.to_s.split(':')
        return Time.new.change(hour:(str_list[0].to_i + 12), min:str_list[1].to_i)
      end
 
      # 파싱 안됨
      return nil
    end
cs
 

 

 

완성

 

소스코드

https://github.com/YoonShinWoong/Chat_Appointment_recognition

 

YoonShinWoong/Chat_Appointment_recognition

Recognizes the contents of the chat about the appointment location or appointment date and sends an alarm - YoonShinWoong/Chat_Appointment_recognition

github.com

 

후기

여전히 개선점이 너무나 많고 완성도도 많이 부족했지만, 여러 가지로 의미 있던 경험이었습니다.

 

가장 먼저, Rails를 안 쓴지 꽤 많은 시간이 지났었는데, 이번 기회에 rails를 다시 써보면서 공부를 할 수 있었습니다. 다양한 레퍼런스들을 보고 또 구현해보면서 'Model의 컬럼 변경 같은 것도 커맨드로 할 수 있었네...' 등 정말 빠른 개발을 위한 환경이 잘 구비되어 있었습니다. 그 외 seed나 scaffolding 등 빠르고 손쉽게 구현할 수 있도록 환경이 잘 갖추어져 있었습니다. 

 

그리고 기능을 구현해보면서 해당 기능에 대해서 조금 더 깊은 분석과 탄탄한 설계가 필요하다고 생각했습니다. 유저가 어떻게 이 기능을 활용할 수 있을지, 또 정말 필요한 기능인지 이런 의문점이 많이 들었습니다. 구현 전에는 '이런 기능 있으면 재밌을 거 같은데...' 이런 생각이었지만, 구현하다보니 '아 여러가지 변수를 고려하다 보니 기능 자체가 무거워질텐데 기능 자체가 무거운 연산들을 감내할만큼의 값어치가 있을까?' 이런 생각들도 들었습니다. 

 

결론적으로 짧은 시간이었지만 스스로 부족함도 많이 느끼고 많이 배울 수 있었던 경험이었습니다. 무엇보다도 스스로 '이런 문제는 어떻게 해결할 수 있을까' 와 같은 의문점들을 던지고 그 답을 찾아나가는 과정에서 많이 배울 수 있었던 경험이었습니다. 

 

 

참고