-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDateTimeHelper.cs
More file actions
242 lines (211 loc) · 10.1 KB
/
Copy pathDateTimeHelper.cs
File metadata and controls
242 lines (211 loc) · 10.1 KB
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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
using System;
using System.Globalization;
namespace NewSchool;
/// <summary>
/// 프로젝트 전체에서 사용할 DateTime 헬퍼 클래스
/// ISO 8601/RFC 3339 형식으로 통일
/// </summary>
public static class DateTimeHelper
{
// 학기 시작일 기준 주차를 세던 WeekNumber 는 호출부가 없어 지웠다(2026-09-04).
// 연간 수업 계획의 주차 표시에 쓰려던 것인데, 그 화면은 계획 행이 들고 있는
// 주차 값을 그대로 쓴다(날짜에서 되계산하지 않는다).
/// <summary>
/// 그 날짜가 속한 학기(1 또는 2). **1학기 = 3~8월, 2학기 = 9~다음해 2월**.
///
/// <para>규칙을 화면마다 적지 말 것 — 실제로 어긋나 있었다. 초기 설정 창은 이 규칙을
/// 그대로 썼는데 누가기록 입력 상자만 <c>Month <= 6</c> 이라, <b>7·8월에 쓴 1학기
/// 기록이 2학기로, 1·2월에 쓴 2학기 기록이 1학기로</b> 기본 선택됐다. 기록 조회는 학기로
/// 거르므로 그렇게 저장된 기록은 제 학기 목록에서 사라진다.</para>
///
/// <para>정본은 <see cref="Services.WeeklyHoursCalculator.DefaultSemesterRange"/> 다
/// (1학기 3월 시작·2학기 9월 시작을 테스트가 고정해 두었다). 여기서는 그 관례를
/// "날짜 → 학기" 방향으로만 쓴다. 실제 학기 구간은 학사일정에서 유추하는 쪽이 정확하므로,
/// 시수 계산처럼 구간이 필요한 곳은 <c>ResolveSemesterRange</c> 를 쓴다.</para>
/// </summary>
public static int SemesterOf(DateTime date) => date.Month is >= 3 and <= 8 ? 1 : 2;
/// <summary>
/// 그 날짜가 속한 <b>학년도</b>. 학년도는 3월에 시작하므로 1·2월은 지난해 학년도다.
///
/// <para>NEIS 학사일정을 내려받는 곳들이 저마다 <c>DateTime.Today.Year</c> 를 그대로
/// 학년도로 넘겼는데, 1·2월에는 그것이 아직 시작하지도 않은 학년도라 조회가 비었다.</para>
/// </summary>
public static int SchoolYearOf(DateTime date) => date.Month >= 3 ? date.Year : date.Year - 1;
/// <summary>
/// 그 날짜가 속한 주의 월요일. 주 단위 시간표가 어느 주를 펼칠지 정하는 기준이다.
/// (수업 홈의 내 시간표와 주간 시간표 화면이 각자 같은 코드를 들고 있었다 — 한 벌로 모은다.)
/// </summary>
public static DateTime MondayOf(DateTime date)
{
var monday = date.Date;
while (monday.DayOfWeek != DayOfWeek.Monday)
monday = monday.AddDays(-1);
return monday;
}
#region 상수 정의
/// <summary>
/// DB 저장 및 API 통신용 표준 형식 (ISO 8601/RFC 3339)
/// </summary>
public const string STANDARD_FORMAT = "yyyy-MM-ddTHH:mm:ss.fffZ";
/// <summary>
/// 날짜만 저장할 때 사용
/// </summary>
public const string DATE_ONLY_FORMAT = "yyyy-MM-dd";
/// <summary>
/// NEIS API용 날짜 형식
/// </summary>
public const string NEIS_DATE_FORMAT = "yyyyMMdd";
// 사용자 표시용 형식 셋(DISPLAY_DATETIME_FORMAT·DISPLAY_DATE_FORMAT·DISPLAY_TIME_FORMAT)은
// 이를 쓰던 ToDisplayString 과 함께 지웠다(2026-09-28, 아래 유틸리티 메서드 참고).
#endregion
#region DateTime → String 변환 (저장용)
/// <summary>
/// DateTime을 표준 문자열로 변환 (DB 저장, API 통신용)
/// UTC로 변환 후 RFC 3339 형식으로 저장
/// </summary>
public static string ToStandardString(DateTime dateTime)
{
try
{
// Local 또는 Unspecified → UTC로 변환
var utcTime = dateTime.Kind switch
{
DateTimeKind.Utc => dateTime,
DateTimeKind.Local => dateTime.ToUniversalTime(),
DateTimeKind.Unspecified => DateTime.SpecifyKind(dateTime, DateTimeKind.Local).ToUniversalTime(),
_ => dateTime.ToUniversalTime()
};
return utcTime.ToString(STANDARD_FORMAT, CultureInfo.InvariantCulture);
}
catch (Exception ex)
{
NewSchool.Logging.Log.Warning("DateTimeHelper", $"날짜를 문자열로 바꾸지 못해 최솟값으로 적는다: {ex.Message}");
return DateTime.MinValue.ToString(STANDARD_FORMAT, CultureInfo.InvariantCulture);
}
}
/// <summary>
/// DateTimeOffset을 표준 문자열로 변환
/// </summary>
public static string ToStandardString(DateTimeOffset dateTimeOffset)
{
return dateTimeOffset.UtcDateTime.ToString(STANDARD_FORMAT, CultureInfo.InvariantCulture);
}
// 변환 넷(ToDateString·ToNeisDateString·NowToStandardString·UtcNowToStandardString)은
// 호출부가 없어 지웠다(39차). 화면·리포지토리는 필요한 자리에서 직접 서식을 지정한다.
#endregion
#region String → DateTime 변환 (로드용)
/// <summary>
/// 문자열을 DateTime으로 변환 (DB 로드, API 응답 파싱용)
/// 다양한 형식 자동 인식 및 Local 시간으로 변환
/// </summary>
public static DateTime FromString(string dateTimeString)
{
if (string.IsNullOrWhiteSpace(dateTimeString))
{
System.Diagnostics.Debug.WriteLine($"[DateTimeHelper] FromString: 빈 문자열");
return DateTime.MinValue;
}
// 지원하는 날짜 형식들 (우선순위 순)
string[] formats = new[]
{
STANDARD_FORMAT, // RFC 3339 (표준)
"yyyy-MM-ddTHH:mm:ssZ", // RFC 3339 (초 단위)
"yyyy-MM-ddTHH:mm:ss.ffffffZ", // RFC 3339 (마이크로초)
"yyyy-MM-ddTHH:mm:ss.fffffffZ", // RFC 3339 (나노초)
"yyyy-MM-dd HH:mm:ss.FFFFFFF", // 레거시 DB 형식
"yyyy-MM-dd HH:mm:ss.FFFFFF",
"yyyy-MM-dd HH:mm:ss.FFFFF",
"yyyy-MM-dd HH:mm:ss.FFFF",
"yyyy-MM-dd HH:mm:ss.FFF",
"yyyy-MM-dd HH:mm:ss.FF",
"yyyy-MM-dd HH:mm:ss.F",
"yyyy-MM-dd HH:mm:ss" // 기본 형식
};
// 형식별로 파싱 시도
// 성공 시 로그는 남기지 않음 — 리포지토리 대량 로드에서 행마다 호출되어
// Debug.WriteLine 이 심각한 병목이 됐었음(실패만 로그)
foreach (var format in formats)
{
if (DateTime.TryParseExact(dateTimeString, format,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var result))
{
// UTC → Local 시간으로 변환
return result.ToLocalTime();
}
}
// 날짜만 있는 형식은 UTC 가정 없이 그대로 파싱
// (AssumeUniversal 로 처리하면 로컬 변환 시 시각이 붙고, 음수 오프셋 시간대에선 날짜가 밀림)
string[] dateOnlyFormats = { DATE_ONLY_FORMAT, NEIS_DATE_FORMAT };
foreach (var format in dateOnlyFormats)
{
if (DateTime.TryParseExact(dateTimeString, format,
CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateOnly))
{
return dateOnly;
}
}
// 폴백: 일반 파싱 시도
if (DateTime.TryParse(dateTimeString,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var fallbackResult))
{
return fallbackResult.ToLocalTime();
}
System.Diagnostics.Debug.WriteLine($"[DateTimeHelper] 변환 실패: '{dateTimeString}'");
return DateTime.MinValue;
}
/// <summary>
/// 날짜 문자열을 DateTime으로 변환
/// </summary>
public static DateTime FromDateString(string dateString)
{
if (string.IsNullOrWhiteSpace(dateString))
return DateTime.MinValue;
// 날짜 형식들
string[] dateFormats = new[]
{
DATE_ONLY_FORMAT, // yyyy-MM-dd
NEIS_DATE_FORMAT, // yyyyMMdd
"yyyy/MM/dd",
"yyyy.MM.dd"
};
foreach (var format in dateFormats)
{
if (DateTime.TryParseExact(dateString, format,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var result))
{
return result;
}
}
// 폴백 — 이 파일의 다른 파싱과 같이 InvariantCulture 로 통일한다.
// 여기만 현재 문화권을 쓰고 있어서, 로캘에 따라 "01/02/2026" 의 월·일 해석이
// 달라질 수 있었다(DB 에 들어간 값은 문화권과 무관한데도).
if (DateTime.TryParse(dateString, CultureInfo.InvariantCulture,
DateTimeStyles.None, out var fallback))
return fallback;
return DateTime.MinValue;
}
#endregion
#region 유틸리티 메서드
// 같은 날 비교(IsSameDay)와 하루의 시작(ToStartOfDay)은 호출부가 없어 지웠다(39차) —
// 코드에서는 `.Date` 를 직접 비교한다. 같은 까닭으로 오늘인지(IsToday)·유효한지(IsValid)
// 도 지웠다(2026-09-28).
/// <summary>
/// 날짜를 하루의 끝 시간으로 설정 (23:59:59.999)
/// </summary>
public static DateTime ToEndOfDay(DateTime dateTime)
{
return dateTime.Date.AddDays(1).AddMilliseconds(-1);
}
// 사용자 표시용 문자열(ToDisplayString)과 상대 시간("5분 전", ToRelativeTimeString)은
// 지웠다(2026-09-28) — 부르는 곳이 확장 메서드 판뿐이었고 그 확장도 아무도 쓰지 않았다.
// 화면은 각자 필요한 형식으로 ToString 한다.
#endregion
}
// 확장 메서드 판(DateTimeExtensions: ToStandardString 둘·ToDisplayString·ToRelativeTimeString)은
// 호출부가 없어 지웠다(2026-09-28). 저장용 문자열은 DateTimeHelper.ToStandardString 을 직접 부른다.