WeddingThankYou 1.0.0.0
결혼식 이후 감사 인사와 사진, 계좌 안내, 연락처를 모바일 페이지로 전달하는 감사장 서비스입니다.
로딩중...
검색중...
일치하는것 없음
CsvGuestbookStorage.cs
이 파일의 문서화 페이지로 가기
1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.IO;
5using System.Text;
6using System.Threading;
7using System.Threading.Tasks;
8using Microsoft.Extensions.Hosting;
10
12{
22 {
31 private readonly string _dataDir;
40 private static readonly SemaphoreSlim _gate = new(1, 1);
41
58 public CsvGuestbookStorage(IHostEnvironment env)
59 {
60 _dataDir = Path.Combine(env.ContentRootPath, "App_Data", "Guestbook");
61 Directory.CreateDirectory(_dataDir);
62 }
63
88 private string CsvPath(string slug) =>
89 Path.Combine(_dataDir, $"{Sanitize(slug)}.csv");
90
115 private static string Sanitize(string slug) =>
116 string.Concat((slug ?? string.Empty).ToLowerInvariant().Split(Path.GetInvalidFileNameChars()));
117
150 public async Task<IReadOnlyList<GuestbookEntry>> LoadAsync(string slug, CancellationToken ct = default)
151 {
152 var csvPath = CsvPath(slug);
153 await _gate.WaitAsync(ct).ConfigureAwait(false);
154 try
155 {
156 var result = new List<GuestbookEntry>();
157 if (!File.Exists(csvPath)) return result;
158
159 using var fs = new FileStream(csvPath, FileMode.Open, FileAccess.Read, FileShare.Read);
160 using var sr = new StreamReader(fs, Encoding.UTF8);
161 string? line;
162
163 // 헤더 지원: 첫 줄이 헤더 같으면 스킵
164 bool first = true;
165 while ((line = await sr.ReadLineAsync().ConfigureAwait(false)) != null)
166 {
167 ct.ThrowIfCancellationRequested();
168 if (string.IsNullOrWhiteSpace(line)) continue;
169
170 if (first && IsHeader(line)) { first = false; continue; }
171 first = false;
172
173 var f = ParseCsvLine(line);
174 if (f.Count < 4) continue;
175
176 if (!DateTime.TryParseExact(f[3], "yyyy-MM-dd HH:mm:ss",
177 CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var when))
178 when = DateTime.Now;
179
180 result.Add(new GuestbookEntry
181 {
182 Name = f[0],
183 Contact = f[1],
184 Message = f[2],
185 CreatedLocal = when
186 });
187 }
188 return result;
189 }
190 finally
191 {
192 _gate.Release();
193 }
194 }
195
236 public async Task SaveAsync(string slug, IReadOnlyList<GuestbookEntry> entries, CancellationToken ct = default)
237 {
238 var csvPath = CsvPath(slug);
239 await _gate.WaitAsync(ct).ConfigureAwait(false);
240 try
241 {
242 var tmp = csvPath + ".tmp";
243 using (var fs = new FileStream(tmp, FileMode.Create, FileAccess.Write, FileShare.None))
244 using (var sw = new StreamWriter(fs, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)))
245 {
246 // 헤더 작성
247 await sw.WriteLineAsync("Name,Contact,Message,CreatedLocal").ConfigureAwait(false);
248
249 foreach (var e in entries)
250 {
251 ct.ThrowIfCancellationRequested();
252 var line = string.Join(",",
253 EscapeCsv(e.Name),
254 EscapeCsv(e.Contact),
255 EscapeCsv(e.Message),
256 EscapeCsv(e.CreatedLocal.ToString("yyyy-MM-dd HH:mm:ss")));
257 await sw.WriteLineAsync(line).ConfigureAwait(false);
258 }
259 }
260
261 File.Copy(tmp, csvPath, overwrite: true);
262 File.Delete(tmp);
263 }
264 finally
265 {
266 _gate.Release();
267 }
268 }
269
294 private static bool IsHeader(string line)
295 {
296 var l = line.Trim().ToLowerInvariant();
297 return l.StartsWith("name,contact,message,createdlocal");
298 }
299
324 private static string EscapeCsv(string value)
325 {
326 value ??= string.Empty;
327 bool needQuote = value.Contains(',') || value.Contains('"') || value.Contains('\n') || value.Contains('\r');
328 if (!needQuote) return value;
329 return $"\"{value.Replace("\"", "\"\"")}\"";
330 }
331
356 private static List<string> ParseCsvLine(string line)
357 {
358 var list = new List<string>();
359 if (string.IsNullOrEmpty(line)) return list;
360
361 var sb = new StringBuilder();
362 bool inQuotes = false;
363
364 for (int i = 0; i < line.Length; i++)
365 {
366 var ch = line[i];
367 if (inQuotes)
368 {
369 if (ch == '"')
370 {
371 if (i + 1 < line.Length && line[i + 1] == '"')
372 {
373 sb.Append('"'); i++; // "" -> "
374 }
375 else inQuotes = false;
376 }
377 else sb.Append(ch);
378 }
379 else
380 {
381 if (ch == ',') { list.Add(sb.ToString()); sb.Clear(); }
382 else if (ch == '"') inQuotes = true;
383 else sb.Append(ch);
384 }
385 }
386 list.Add(sb.ToString());
387 return list;
388 }
389 }
390}
async Task SaveAsync(string slug, IReadOnlyList< GuestbookEntry > entries, CancellationToken ct=default)
async Task< IReadOnlyList< GuestbookEntry > > LoadAsync(string slug, CancellationToken ct=default)
static List< string > ParseCsvLine(string line)