Families.AutoWriter 1.0.0.0
Families 콘텐츠를 반복 작업 없이 작성·등록하도록 돕는 자동화 작성 도구입니다.
로딩중...
검색중...
일치하는것 없음
FamiliesAutoWriter.ViewModels.WriterSession 클래스 참조sealed

더 자세히 ...

FamiliesAutoWriter.ViewModels.WriterSession에 대한 상속 다이어그램 :
FamiliesAutoWriter.ViewModels.WriterSession에 대한 협력 다이어그램:

Public 멤버 함수

 WriterSession (string? initialPrompt=null)
void StopAutomation ()

정적 Public 멤버 함수

static string BuildTravelPrompt (IReadOnlyList< string > existingTitles)
static string BuildCookingPrompt (IReadOnlyList< string > existingTitles)
static string BuildCookingPrompt (IReadOnlyList< string > existingTitles,(DateTime? LastPostedAt, int LastNumber) timeline)

속성

SessionMode Mode = SessionMode.Travel [get, set]
Func< string, Task< string?> >? ExecuteScriptAsync [get, set]
Action< string >? NavigateTab [get, set]
ObservableCollection< string > Slugs = [] [get]
ObservableCollection< AlbumInfoAlbums = [] [get]
static IntervalOption[] IntervalOptions [get]
static WaitOption[] WaitOptions [get]
static int[] NewChatOptions = [0, 3, 5, 10, 20, 30] [get]
string AppDataRoot [get, set]
string SelectedSlug [get, set]
AlbumInfoSelectedAlbum [get, set]
bool LoopEnabled [get, set]
string LoopLabel [get]
IntervalOption SelectedInterval [get, set]
WaitOption AiWaitOption [get, set]
int IntervalMinutes [get]

Private 멤버 함수

void RefreshSlugList ()
void RunNow ()
void SendPromptNow ()
void ExtractAndSave ()
void ClearPromptHistory ()
void BrowseAppData ()
void RefreshSlugs ()
void RefreshAlbums ()
void RefreshPromptWithExisting ()
void StartLoop ()
void StopLoop ()
async Task LoopWorkerAsync (CancellationToken ct)
async Task RunLoopCycleAsync (CancellationToken ct)
Task SetStatus (string msg)
void RotateAlbum ()
async Task SendPromptNowAsync ()
async Task ExtractAndSaveAsync ()
record AiPostResult (string Title, string Content, List< string > Photos, List< string > Videos)
async Task< List< PostEntry > > ParseCookingResponseAsync (string? raw)

정적 Private 멤버 함수

static string UnwrapRaw (string? raw)
static string GetSection (string text, string tag)
static string SubstituteImages (string content, IList< string > urls, int offset=0)
static ? AiPostResult ParseTravelResponse (string? raw)
static DateTime GetNextCookingSlot (DateTime? lastPostedAt)
static string GetCookingMealName (DateTime postedAt)
static string NormalizeCookingTitle (string title, int number, string mealName)
static int ExtractCookingNumberFromTitle (string title)
static List< string > ExtractImageUrls (string content)
static string RemoveInvalidInlineImages (string content, IReadOnlyCollection< string > allowedUrls)
static List< string > MergeUrls (IEnumerable< string > primary, IEnumerable< string > secondary)
static string NormalizeTravelContent (string content)
static bool IsAllowedVideoUrl (string url)
static string NormalizeCookingContent (string content)
static string NormalizeKnownHeadings (string content, IReadOnlyDictionary< string, string > headings)
static string NormalizeSimpleTables (string content)
static string NormalizeImageBlockSpacing (string content)
static void AddBlankLineIfNeeded (List< string > lines)
static List< string > TryExpandCollapsedPipeTable (string line)
static List< string > SplitTableLine (string line)
static string NormalizeIngredientLines (string content)
static bool IsPlausibleImageUrl (string url)
static bool IsLikelyDirectImageUrl (string url)
static async Task< List< string > > ValidateImageUrlsAsync (List< string > urls, Action< string >? onStatus=null, ISet< string >? excludeUrls=null)
static async Task< bool > IsReachableImageAsync (string url, HttpMethod method)
static string BuildCookingTimelineInstruction ((DateTime? LastPostedAt, int LastNumber) timeline)
static string DetectDefaultRoot ()

Private 속성

readonly PostWriterService _writer = new()
readonly PromptHistoryService _history = new()
readonly System.Windows.Threading.Dispatcher _uiDispatcher
DateTime _nextLoopAt = DateTime.MaxValue
CancellationTokenSource? _loopCts
Task? _loopTask
int _cycleCount
readonly HashSet< string > _usedImageUrls = new(StringComparer.OrdinalIgnoreCase)
int _albumIndex
bool _albumRotationEnabled
int _newChatEveryN = 5
string _loopStatus = "⏸ 루프 꺼짐"
string _loopCountdown = ""
MediaPosition _mediaPosition = MediaPosition.Bottom
string _promptText = BuildTravelPrompt([])
string _promptStatus = ""
string _extractStatus = ""
string _appDataRoot = DetectDefaultRoot()
string _selectedSlug = ""
AlbumInfo_selectedAlbum
bool _loopEnabled
IntervalOption _selectedInterval = null!
WaitOption _aiWaitOption = null!

정적 Private 속성

const int ExtractionRetryDelaySeconds = 30
const int ExtractionRetryMaxAttempts = 10
static readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(15) }
static readonly string[] _trustedImageHosts

상세한 설명

탭 하나에 대응하는 독립 세션 — 슬러그/앨범/프롬프트/루프 상태를 각자 보유

WriterSession.cs 파일의 112 번째 라인에서 정의되었습니다.

생성자 & 소멸자 문서화

◆ WriterSession()

FamiliesAutoWriter.ViewModels.WriterSession.WriterSession ( string? initialPrompt = null)
inline

지정한 설정으로 WriterSession 클래스의 새 인스턴스를 초기화합니다.

매개변수
initialPromptinitial Prompt에 사용할 string? 값입니다.

WriterSession.cs 파일의 553 번째 라인에서 정의되었습니다.

554 {
555 _uiDispatcher = System.Windows.Application.Current.Dispatcher;
556 _selectedInterval = IntervalOptions[1]; // 10분
557 _aiWaitOption = WaitOptions[2]; // 30초
558 if (initialPrompt != null) _promptText = initialPrompt;
559 RefreshSlugs();
560 }

다음을 참조함 : _aiWaitOption, _promptText, _selectedInterval, _uiDispatcher, IntervalOptions, RefreshSlugs(), WaitOptions.

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:

멤버 함수 문서화

◆ AddBlankLineIfNeeded()

void FamiliesAutoWriter.ViewModels.WriterSession.AddBlankLineIfNeeded ( List< string > lines)
inlinestaticprivate

Blank Line If Needed 항목을 추가합니다.

매개변수
lineslines에 사용할 List<string> 값입니다.

WriterSession.cs 파일의 2185 번째 라인에서 정의되었습니다.

2186 {
2187 if (lines.Count > 0 && lines[^1].Trim().Length > 0)
2188 lines.Add("");
2189 }

다음에 의해서 참조됨 : NormalizeImageBlockSpacing().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ AiPostResult()

record FamiliesAutoWriter.ViewModels.WriterSession.AiPostResult ( string Title,
string Content,
List< string > Photos,
List< string > Videos )
sealedprivate

Ai Post Result 기능과 관련 상태를 캡슐화합니다.

다음에 의해서 참조됨 : ParseTravelResponse().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ BrowseAppData()

void FamiliesAutoWriter.ViewModels.WriterSession.BrowseAppData ( )
inlineprivate

Browse App Data 작업을 수행합니다.

WriterSession.cs 파일의 616 번째 라인에서 정의되었습니다.

617 {
618 var dlg = new OpenFolderDialog { Title = "Families.Web App_Data 폴더 선택" };
619 if (dlg.ShowDialog() == true) AppDataRoot = dlg.FolderName;
620 }

다음을 참조함 : AppDataRoot.

◆ BuildCookingPrompt() [1/2]

string FamiliesAutoWriter.ViewModels.WriterSession.BuildCookingPrompt ( IReadOnlyList< string > existingTitles)
inlinestatic

Cooking Prompt 값을 구성합니다.

매개변수
existingTitlesexisting Titles에 사용할 IReadOnlyList<string> 값입니다.
반환값
Build Cooking Prompt 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 2735 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : FamiliesAutoWriter.MainWindow.AddTabFromInputAsync(), RefreshPromptWithExisting().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ BuildCookingPrompt() [2/2]

string FamiliesAutoWriter.ViewModels.WriterSession.BuildCookingPrompt ( IReadOnlyList< string > existingTitles,
(DateTime? LastPostedAt, int LastNumber) timeline )
inlinestatic

Cooking Prompt 값을 구성합니다.

매개변수
existingTitlesexisting Titles에 사용할 IReadOnlyList<string> 값입니다.
timelinetimeline에 사용할 (DateTime? LastPostedAt, int LastNumber) 값입니다.
반환값
Build Cooking Prompt 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 2808 번째 라인에서 정의되었습니다.

2817 =>
2818 $$"""
2819 {앨범} 주제로 1983년 한국 가족 집밥·육아 기록 블로그 포스트를 1개 작성해줘.
2820 아래 형식 그대로만 출력해줘. ===섹션=== 구분자는 반드시 유지하고, 다른 설명은 쓰지 마.
2821 코드블록(```), JSON, 주석, 머리말, 맺음말을 절대 쓰지 마.
2822
2823 AutoWriter 저장 계약:
2824 - 이 응답은 Families.Web App_Data/Family/{선택슬러그}/posts/*.json 으로 파싱 저장된다.
2825 - AutoWriter는 ===IMAGES===의 URL 1개와 ===DETAIL_NNN_TITLE===, ===DETAIL_NNN_DATE===, ===DETAIL_NNN_CONTENT=== 섹션만 실제 포스트로 저장한다.
2826 - DETAIL 번호 NNN은 숫자만 사용한다. 예: 231, 232, 233.
2827 - 이미 작성된 제목 목록에 있는 번호(#숫자), 식사구분, 메뉴명, 비슷한 메뉴는 절대 다시 쓰지 않는다.
2828 - 기존 제목에 "#220"이 있으면 새 제목은 #220을 절대 쓰지 말고, 타임라인 강제 조건의 새 번호만 사용한다.
2829 - 기존 메뉴가 "생선구이"면 고등어구이, 갈치구이, 조기구이처럼 거의 같은 생선구이류도 피한다.
2830 - 기존 메뉴가 "김밥"이면 주먹밥, 유부초밥처럼 같은 도시락/밥말이류 반복도 피한다.
2831 - ALBUMS, TIMELINE, 표 형태 요약은 출력하지 마. 저장되지 않으므로 금지.
2832 - 모든 [대괄호 안내문]은 실제 값으로 바꾼다. 대괄호 안내문을 그대로 남기지 마.
2833 - TITLE은 파일 목록에 보이는 글 제목이므로 한 줄만 작성한다.
2834 - CONTENT는 Markdown 본문이며, 앱이 그대로 렌더링한다.
2835 - CONTENT 안의 제목은 반드시 ##, ###, -, 1. 마크다운 문법을 유지한다.
2836
2837 글쓰기 톤:
2838 - 아기 시점("저는 아직 수유만 해요"), 따뜻하고 구체적인 1983년 가정집 분위기
2839 - 할머니, 아버지, 어머니가 등장하고 각자의 행동과 대화를 자연스럽게 묘사한다
2840 - 도입부는 그날의 계절·날씨·집 분위기로 시작한다 (첫 문단, 섹션 헤더 없이)
2841
2842 메뉴 선택:
2843 - 아침/점심/저녁 중 하나를 고르고, TITLE과 본문 전체에서 같은 식사구분을 유지한다.
2844 - 1983년 한국 집밥 제철 음식으로 쓴다.
2845 - 날짜는 1983년 중 자유롭게 (매 실행마다 다르게)
2846 - 아기에게 직접 먹이는 음식인지 어른 밥상인지 반드시 구분
2847 - TITLE의 식사구분(아침/점심/저녁), 메뉴명, 본문 음식, 이미지 음식은 반드시 서로 일치한다.
2848 - 이번 메뉴는 이미 작성된 메뉴 목록과 겹치지 않는 새 메뉴여야 한다.
2849 - 같은 주재료 반복을 피한다. 최근 목록에 생선 메뉴가 많으면 두부/나물/국/찌개/전/조림/볶음 등 다른 계열로 바꾼다.
2850
2851 레시피 작성 기준 (초보자가 실제로 따라 만들 수 있어야 함):
2852 - 재료는 g, ml, cm 단위로 정확히
2853 - 불 세기(약불/중불/강불)와 조리 시간(분 단위)을 명시
2854 - 익었는지 확인하는 기준을 눈으로 보이는 변화로 설명
2855 - "약간", "적당히", "노릇하게"만 쓰지 말고 반드시 기준 수치를 함께 적는다
2856 - 간이 맞지 않을 때, 탈 것 같을 때 복구 방법 포함
2857
2858 이미지 조건:
2859 - IMAGES 섹션에 메뉴 음식 사진 URL을 6개 출력한다. (검증 실패 대비 여유분)
2860 - 각 URL은 http 또는 https로 시작하는 이미지 직접 URL이어야 한다.
2861 - URL 줄에는 설명, 번호, 마크다운 이미지 문법을 붙이지 말고 URL만 쓴다.
2862 - 메뉴와 다른 음식 사진 금지. 예: 고등어조림 글에 샐러드/덮밥/피자 사진 금지.
2863 - 가로 1000px 이상 선명한 음식 사진만 사용한다. 작은 썸네일, 흐릿한 사진, 생성 이미지 느낌의 사진 금지.
2864 - 실제 브라우저에서 열리는 직접 이미지 URL만 사용한다. 엑박이 뜰 수 있는 URL은 쓰지 않는다.
2865 - URL은 가능하면 .jpg, .jpeg, .png, .webp 로 끝나는 주소를 고른다.
2866 - 우선순위는 upload.wikimedia.org, images.unsplash.com, images.pexels.com 같은 안정적인 이미지 CDN이다.
2867 - 블로그 글 페이지, 검색 결과 페이지, commons.wikimedia.org/wiki/... 페이지, redirect URL, 썸네일 페이지 URL은 금지한다.
2868 - URL에 공백, 한글, 괄호, 따옴표가 있으면 반드시 퍼센트 인코딩된 정상 URL로 출력한다.
2869 - Unsplash/Pexels/Wikimedia/공공 이미지 중 실제 존재하는 직접 이미지 URL만 사용. 가짜 URL 절대 금지.
2870 - 메뉴 사진을 확실히 찾기 어렵다면 그 메뉴를 쓰지 말고, 사진을 확실히 찾을 수 있는 한국 집밥 메뉴로 바꾼다.
2871 - Wikimedia를 쓸 때는 upload.wikimedia.org의 실제 파일 URL을 사용한다. commons.wikimedia.org/wiki/... 페이지 URL은 금지한다.
2872 - Unsplash/Pexels를 쓸 때도 실제 검색으로 확인된 이미지 URL만 쓴다. photo-{실제ID}, {id}, ... 같은 템플릿 URL은 절대 쓰지 않는다.
2873 - DETAIL CONTENT 안에는 <img> 태그를 넣지 마. 이미지는 AutoWriter가 글 상단 미디어로 따로 표시한다.
2874
2875 유튜브: 출력하지 마. VIDEO 섹션에는 항상 "없음"만 쓴다.
2876
2877 {{(existingTitles.Count > 0 ? $"이미 작성된 메뉴 목록 (반드시 제외, 절대 반복 금지):\n{string.Join("\n", existingTitles.TakeLast(120).Select(t => $"- {t}"))}\n" : "")}}
2878 {{BuildCookingTimelineInstruction(timeline)}}
2879
2880 ===DETAIL_[새숫자]_TITLE===
2881 [1983 집밥육아 #새숫자] [아침/점심/저녁] [메뉴명]과 [아기 상태 한 줄]
2882
2883 ===DETAIL_[새숫자]_DATE===
2884 1983년 [날짜]
2885
2886 ===DETAIL_[새숫자]_CONTENT===
2887 [그날의 집 분위기·계절·가족 동선을 자연스럽게 묘사하는 도입 2~3문장. 헤더 없이.]
2888
2889 ## 오늘의 아기 밥
2890
2891 [아기 기록. 수유 또는 이유식 상태를 명확히. 어른 반찬을 직접 먹지 않았음을 명시. 2~3문단.]
2892
2893 ## 오늘의 가족 집밥
2894
2895 [가족 식사 분위기와 음식 이야기. 할머니·아버지·어머니 각자의 모습 묘사. 2~3문단.]
2896
2897 ## 만드는 법
2898
2899 ### 재료: 어른 2~3인분
2900
2901 - [재료명] [정확한 수량 g/ml/개]
2902 - [재료명] [정확한 수량]
2903
2904 ### 손질하기
2905
2906 1. [씻는 방법 구체적으로]
2907 2. [몇 cm 크기로 자르는지]
2908 3. [초보자가 실수하기 쉬운 부분]
2909
2910 ### [조리 방법에 맞는 제목 (볶기/끓이기/부치기 등)]
2911
2912 1. [팬/냄비 예열 시간과 불 세기]
2913 2. [재료 넣는 순서, 불 세기, 시간]
2914 3. [익었는지 확인하는 눈에 보이는 기준]
2915 4. [실패 시 복구 방법]
2916
2917 [간이 짤 때/싱거울 때/탈 것 같을 때 복구 방법을 자연스럽게 이어 쓴다. 헤더 없이 1~2문장.]

◆ BuildCookingTimelineInstruction()

string FamiliesAutoWriter.ViewModels.WriterSession.BuildCookingTimelineInstruction ( (DateTime? LastPostedAt, int LastNumber) timeline)
inlinestaticprivate

Cooking Timeline Instruction 값을 구성합니다.

매개변수
timelinetimeline에 사용할 (DateTime? LastPostedAt, int LastNumber) 값입니다.
반환값
Build Cooking Timeline Instruction 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 2762 번째 라인에서 정의되었습니다.

2772 {
2773 var nextAt = GetNextCookingSlot(timeline.LastPostedAt);
2774 var nextNumber = timeline.LastNumber > 0 ? timeline.LastNumber + 1 : 1;

다음을 참조함 : GetNextCookingSlot().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:

◆ BuildTravelPrompt()

string FamiliesAutoWriter.ViewModels.WriterSession.BuildTravelPrompt ( IReadOnlyList< string > existingTitles)
inlinestatic

Travel Prompt 값을 구성합니다.

매개변수
existingTitlesexisting Titles에 사용할 IReadOnlyList<string> 값입니다.
반환값
Build Travel Prompt 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 2612 번째 라인에서 정의되었습니다.

2612 =>
2613 $$"""
2614 {앨범} 주제로 한국 가족 나들이·여행 블로그 포스트 1개를 작성해줘.
2615 아래 형식 그대로만 출력해줘. ===섹션=== 구분자는 반드시 유지하고, 그 외 설명은 쓰지 마.
2616 코드블록(```), JSON, 주석, 머리말, 맺음말을 절대 쓰지 마.
2617
2618 AutoWriter 저장 계약:
2619 - 이 응답은 Families.Web App_Data/Family/{선택슬러그}/posts/*.json 으로 파싱 저장된다.
2620 - 섹션 태그는 정확히 ===TITLE===, ===CONTENT===, ===IMAGES===, ===VIDEO=== 만 사용한다.
2621 - 모든 [대괄호 안내문]은 실제 값으로 바꾼다. 대괄호 안내문을 그대로 남기지 마.
2622 - TITLE은 파일 목록에 보이는 글 제목이므로 한 줄만 작성한다.
2623 - CONTENT는 Markdown 본문이며, 앱이 그대로 렌더링한다.
2624
2625 작성 조건:
2626 - 실제 국내 장소 1곳을 정한다 (실내·실외 모두 가능).
2627 - 정보는 실제 공식 사이트나 현장 기준으로 구체적으로 적는다.
2628 - 확인이 불가한 정보는 "방문 전 공식 확인 필요"로 표시한다.
2629 - 아이 동반 가족 시선에서 실용적으로 쓴다 (유모차, 수유실, 주차, 대기 등).
2630 - 비용 표는 성인 2명 + 아이 1명 기준으로 작성한다.
2631 - 여행 경비 예상 표는 반드시 Markdown 표로 출력한다. 표 전체를 한 줄에 붙여 쓰지 말고, 헤더/구분선/각 항목 행을 각각 줄바꿈한다.
2632
2633 이미지 조건:
2634 - IMAGES 섹션에 실제 접근 가능한 이미지 URL을 6개 출력한다. "없음" 금지. (많이 줄수록 좋음 — 검증 실패 대비 여유분)
2635 - 각 URL은 http 또는 https로 시작하는 이미지 직접 URL이어야 한다.
2636 - URL 줄에는 설명, 번호, 마크다운 이미지 문법을 붙이지 말고 URL만 쓴다.
2637 - 실제 브라우저에서 열리는 직접 이미지 URL만 사용한다. 엑박이 뜰 수 있는 URL은 쓰지 않는다.
2638 - URL은 가능하면 .jpg, .jpeg, .png, .webp 로 끝나는 주소를 고른다.
2639 - URL에 공백, 한글, 괄호, 따옴표가 있으면 반드시 퍼센트 인코딩된 정상 URL로 출력한다.
2640 - 블로그 글 페이지, 검색 결과 페이지, redirect URL, 썸네일 페이지 URL은 금지한다.
2641 - 가로 1000px 이상 선명한 대표 이미지를 고른다. 작은 썸네일, 흐릿한 사진, 야간 저화질 사진은 금지.
2642 - 해당 장소 공식 사이트의 큰 이미지 또는 장소 안내 이미지/지도/전시 이미지를 1순위로 사용한다.
2643 - 공식 이미지가 작거나 흐리면 Wikimedia, 공공기관 보도자료, Unsplash/Pexels의 실제 큰 이미지로 대체한다.
2644 - 이미지 URL은 반드시 해당 장소 자체를 보여줘야 한다. 장소와 무관한 교통수단, 음식, 사람, 일반 풍경 사진은 금지한다.
2645 - Wikimedia를 쓸 때는 upload.wikimedia.org의 실제 파일 URL을 사용한다. commons.wikimedia.org/wiki/... 페이지 URL은 금지한다.
2646 - Unsplash/Pexels를 쓸 때도 실제 검색으로 확인된 이미지 URL만 쓴다. photo-{실제ID}, {id}, ... 같은 템플릿 URL은 절대 쓰지 않는다.
2647 - CONTENT 안 <img> 태그 src에도 같은 URL을 그대로 사용한다.
2648
2649 유튜브:
2650 - 기본값은 "없음"이다.
2651 - YouTube를 넣어야 한다면 embed 재생이 허용되는 공식 채널/공공기관 영상 1개만 사용한다.
2652 - 개인 방문 영상, Shorts, 광고성 영상, iframe 재생 차단 가능성이 큰 영상은 쓰지 말고 "없음"만 쓴다.
2653
2654 {{(existingTitles.Count > 0 ? $"이미 작성된 장소 목록 (반드시 제외, 절대 반복 금지):\n{string.Join("\n", existingTitles.TakeLast(80).Select(t => $"- {t}"))}\n" : "")}}
2655 ===TITLE===
2656 실제 장소명, 아이 동반 핵심 팁 또는 주의사항
2657
2658 ===CONTENT===
2659
2660 ## 한눈에 보는 포인트
2661
2662 이 장소가 아이 동반 가족에게 좋은 이유를 2~3줄로 작성한다.
2663 이동 동선, 체류 시간, 날씨 조건 등 실용 정보 위주로 쓴다.
2664
2665 <img class="fa-inline-photo" src="[IMAGES 첫 번째 URL]" alt="[장소 대표 이미지 설명]">
2666
2667 ## 이런 가족에게 추천
2668
2669 아이 연령대, 이동 수단, 부모 성향 기준으로 추천 대상을 구체적으로 작성한다.
2670
2671 ## 아이와 좋았던 점
2672
2673 아이가 지루하지 않을 요소, 실제 체험 동선, 부모가 편했던 포인트를 작성한다.
2674 유모차 이용 여부, 수유실/화장실 위치, 그늘/쉬는 공간도 포함한다.
2675
2676 <img class="fa-inline-photo" src="[IMAGES 두 번째 URL]" alt="[장소 내부 또는 체험 이미지 설명]">
2677
2678 ## 동선과 시간표
2679
2680 도착부터 출발까지 시간 흐름에 맞게 이동 순서를 작성한다.
2681 구체적인 시간대(예: 11시 도착, 12시 점심)와 이유를 함께 쓴다.
2682
2683 ## 여행 경비 예상
2684
2685 | 항목 | 예상 비용 | 메모 |
2686 | --- | ---: | --- |
2687 | 입장료 | [실제 금액 또는 무료] | 성인/아이 구분 |
2688 | 주차 | [실제 금액] | 시간당 기준 |
2689 | 식사 | [실제 금액] | 가족 3인 기준 |
2690 | 간식 | [실제 금액] | 선택 항목 |
2691 | 합계 | [실제 금액] | 주차 할인 전 기준 |
2692
2693 <img class="fa-inline-photo" src="[IMAGES 세 번째 URL]" alt="[주변 풍경 또는 식사 관련 이미지 설명]">
2694
2695 ## 주차·화장실·유모차 포인트
2696
2697 주차 위치와 요금, 화장실과 수유실 위치, 유모차 반입/대여 여부를 구체적으로 쓴다.
2698
2699 ## 준비물과 주의할 것
2700
2701 챙기면 좋은 것, 주의해야 할 것을 항목별로 작성한다.
2702
2703 ## 방문 전 확인할 것
2704
2705 운영시간, 예약 여부, 계절적 제한, 바뀔 수 있는 정보를 명시한다.
2706
2707 ## 총평
2708
2709 가족 나들이 장소로서 솔직한 평가를 2~3줄로 작성한다. 추천 대상과 비추 상황도 포함한다.

다음에 의해서 참조됨 : FamiliesAutoWriter.MainWindow.AddTabFromInputAsync(), RefreshPromptWithExisting().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ ClearPromptHistory()

void FamiliesAutoWriter.ViewModels.WriterSession.ClearPromptHistory ( )
inlineprivate

Clear Prompt History 작업을 수행합니다.

WriterSession.cs 파일의 607 번째 라인에서 정의되었습니다.

607{ _history.Clear(); PromptStatus = "🗑 초기화"; }

다음을 참조함 : _history.

◆ DetectDefaultRoot()

string FamiliesAutoWriter.ViewModels.WriterSession.DetectDefaultRoot ( )
inlinestaticprivate

Detect Default Root 작업을 수행합니다.

반환값
Detect Default Root 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 2935 번째 라인에서 정의되었습니다.

◆ ExtractAndSave()

void FamiliesAutoWriter.ViewModels.WriterSession.ExtractAndSave ( )
inlineprivate

Extract And Save 작업을 수행합니다.

WriterSession.cs 파일의 598 번째 라인에서 정의되었습니다.

598=> _ = ExtractAndSaveAsync();

◆ ExtractAndSaveAsync()

async Task FamiliesAutoWriter.ViewModels.WriterSession.ExtractAndSaveAsync ( )
inlineprivate

Extract And Save Async 작업을 수행합니다.

반환값
Extract And Save Async 작업에서 생성한 Task 결과입니다.

WriterSession.cs 파일의 1137 번째 라인에서 정의되었습니다.

1138 {
1139 if (string.IsNullOrWhiteSpace(SelectedSlug)) { ExtractStatus = "❌ 슬러그를 선택하세요."; return; }
1140 if (ExecuteScriptAsync == null) { ExtractStatus = "❌ 브라우저 준비 안 됨"; return; }
1141
1142 ExtractStatus = "⏳ AI 응답 추출 중...";
1143 var raw = await ExecuteScriptAsync(ResponseExtractor.BuildExtractScript());
1144
1145 try
1146 {
1147 for (var extractAttempt = 1; extractAttempt <= ExtractionRetryMaxAttempts; extractAttempt++)
1148 {
1149 if (Mode == SessionMode.Cooking)
1150 {
1151 var posts = await ParseCookingResponseAsync(raw);
1152 if (posts.Count == 0)
1153 {
1154 if (extractAttempt >= ExtractionRetryMaxAttempts)
1155 {
1156 ExtractStatus = "❌ 요리 포맷 추출 실패 (===DETAIL_NNN_CONTENT=== 없음)";
1157 return;
1158 }
1159
1160 ExtractStatus = $"⏳ AI 응답 불완전 - {ExtractionRetryDelaySeconds}초 후 재추출 ({extractAttempt}/{ExtractionRetryMaxAttempts})";
1161 await Task.Delay(TimeSpan.FromSeconds(ExtractionRetryDelaySeconds));
1162 raw = await ExecuteScriptAsync(ResponseExtractor.BuildExtractScript());
1163 continue;
1164 }
1165
1166 var existingCookingTitles = _writer.GetExistingTitles(SelectedSlug);
1167 var existingCookingNumbers = existingCookingTitles
1168 .Select(ExtractCookingNumberFromTitle)
1169 .Where(n => n > 0)
1170 .ToHashSet();
1171 var newPosts = posts.Where(p =>
1172 !existingCookingTitles.Any(t => string.Equals(t, p.Title, StringComparison.OrdinalIgnoreCase)) &&
1173 !existingCookingNumbers.Contains(ExtractCookingNumberFromTitle(p.Title))
1174 ).ToList();
1175 if (newPosts.Count == 0) { ExtractStatus = "⏭ 모두 중복 제목/번호 스킵"; return; }
1176
1177 foreach (var p in newPosts) await _writer.SavePostAsync(SelectedSlug, p, []);
1178 ExtractStatus = $"✅ {newPosts.Count}개 저장! ({DateTime.Now:HH:mm})";
1179 break;
1180 }
1181 else
1182 {
1183 var result = ParseTravelResponse(raw);
1184 if (result is null || string.IsNullOrWhiteSpace(result.Content))
1185 {
1186 if (extractAttempt >= ExtractionRetryMaxAttempts)
1187 {
1188 ExtractStatus = "❌ 응답 없음";
1189 return;
1190 }
1191
1192 ExtractStatus = $"⏳ AI 응답 불완전 - {ExtractionRetryDelaySeconds}초 후 재추출 ({extractAttempt}/{ExtractionRetryMaxAttempts})";
1193 await Task.Delay(TimeSpan.FromSeconds(ExtractionRetryDelaySeconds));
1194 raw = await ExecuteScriptAsync(ResponseExtractor.BuildExtractScript());
1195 continue;
1196 }
1197
1198 var albumName = SelectedAlbum?.Name ?? "전체";
1199 List<string> validPhotos = [];
1200 if (result.Photos.Count > 0)
1201 {
1202 ExtractStatus = "🔍 이미지 검증...";
1203 var allValid = await ValidateImageUrlsAsync(result.Photos, msg => ExtractStatus = msg);
1204 validPhotos = allValid.Take(3).ToList(); // 여행: 최대 3개
1205 }
1206 var content = NormalizeTravelContent(RemoveInvalidInlineImages(result.Content, validPhotos));
1207
1208 var post = new PostEntry
1209 {
1210 Title = !string.IsNullOrWhiteSpace(result.Title) ? result.Title : $"[{albumName}] {DateTime.Now:MM/dd HH:mm}",
1211 Content = content,
1212 AlbumId = SelectedAlbum?.Id ?? "",
1214 PostedAt = DateTime.Now,
1215 PhotoFileNames = validPhotos,
1216 VideoFileNames = result.Videos,
1217 };
1218 await _writer.SavePostAsync(SelectedSlug, post, []);
1219 ExtractStatus = $"✅ [{albumName}] 저장! ({DateTime.Now:HH:mm}) 📷{validPhotos.Count} 🎬{result.Videos.Count}";
1220 break;
1221 }
1222 }
1223 }
1224 catch (Exception ex) { ExtractStatus = $"❌ {ex.Message}"; }
1225 }

다음을 참조함 : _writer, FamiliesAutoWriter.Services.ResponseExtractor.BuildExtractScript(), ExecuteScriptAsync, ExtractCookingNumberFromTitle(), ExtractionRetryDelaySeconds, ExtractionRetryMaxAttempts, Mode, NormalizeTravelContent(), ParseCookingResponseAsync(), ParseTravelResponse(), RemoveInvalidInlineImages(), SelectedAlbum, SelectedSlug, ValidateImageUrlsAsync().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:

◆ ExtractCookingNumberFromTitle()

int FamiliesAutoWriter.ViewModels.WriterSession.ExtractCookingNumberFromTitle ( string title)
inlinestaticprivate

Extract Cooking Number From Title 작업을 수행합니다.

매개변수
titletitle에 사용할 string 값입니다.
반환값
Extract Cooking Number From Title 작업에서 생성한 int 결과입니다.

WriterSession.cs 파일의 1715 번째 라인에서 정의되었습니다.

1716 {
1717 var match = Regex.Match(title ?? "", @"#(?<n>\d+)");
1718 return match.Success && int.TryParse(match.Groups["n"].Value, out var number) ? number : 0;
1719 }

다음에 의해서 참조됨 : ExtractAndSaveAsync(), RunLoopCycleAsync().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ ExtractImageUrls()

List< string > FamiliesAutoWriter.ViewModels.WriterSession.ExtractImageUrls ( string content)
inlinestaticprivate

Extract Image Urls 작업을 수행합니다.

매개변수
contentcontent에 사용할 string 값입니다.
반환값
Extract Image Urls 작업에서 생성한 List<string> 결과입니다.

WriterSession.cs 파일의 1745 번째 라인에서 정의되었습니다.

1745 =>
1746 Regex.Matches(content, @"<img\b[^>]*\bsrc\s*=\s*[""'](?<url>https?://[^""']+)[""'][^>]*>", RegexOptions.IgnoreCase)
1747 .Select(m => System.Net.WebUtility.HtmlDecode(m.Groups["url"].Value.Trim()))
1748 .Where(u => u.StartsWith("http", StringComparison.OrdinalIgnoreCase))
1749 .ToList();

다음에 의해서 참조됨 : ParseCookingResponseAsync(), ParseTravelResponse().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ GetCookingMealName()

string FamiliesAutoWriter.ViewModels.WriterSession.GetCookingMealName ( DateTime postedAt)
inlinestaticprivate

Cooking Meal Name 값을 가져옵니다.

매개변수
postedAtposted At에 사용할 DateTime 값입니다.
반환값
Get Cooking Meal Name 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 1620 번째 라인에서 정의되었습니다.

1620 =>
1621 postedAt.Hour switch
1622 {
1623 < 12 => "아침",
1624 < 18 => "점심",
1625 _ => "저녁",
1626 };

다음에 의해서 참조됨 : ParseCookingResponseAsync().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ GetNextCookingSlot()

DateTime FamiliesAutoWriter.ViewModels.WriterSession.GetNextCookingSlot ( DateTime? lastPostedAt)
inlinestaticprivate

Next Cooking Slot 값을 가져옵니다.

매개변수
lastPostedAtlast Posted At에 사용할 DateTime? 값입니다.
반환값
Get Next Cooking Slot 작업에서 생성한 DateTime 결과입니다.

WriterSession.cs 파일의 1582 번째 라인에서 정의되었습니다.

1583 {
1584 if (lastPostedAt is null)
1585 return new DateTime(1983, 1, 1, 8, 0, 0);
1586
1587 var last = lastPostedAt.Value;
1588 return last.Hour switch
1589 {
1590 < 12 => last.Date.AddHours(12),
1591 < 18 => last.Date.AddHours(18),
1592 _ => last.Date.AddDays(1).AddHours(8),
1593 };
1594 }

다음에 의해서 참조됨 : BuildCookingTimelineInstruction(), ParseCookingResponseAsync().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ GetSection()

string FamiliesAutoWriter.ViewModels.WriterSession.GetSection ( string text,
string tag )
inlinestaticprivate

Section 값을 가져옵니다.

매개변수
texttext에 사용할 string 값입니다.
tagtag에 사용할 string 값입니다.
반환값
Get Section 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 1309 번째 라인에서 정의되었습니다.

1310 {
1311 var marker = $"==={tag}===";
1312 var s = text.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
1313 if (s < 0) return "";
1314 s += marker.Length;
1315 var n = text.IndexOf("===", s);
1316 return text[s..(n > s ? n : text.Length)].Trim();
1317 }

다음에 의해서 참조됨 : ParseCookingResponseAsync(), ParseTravelResponse().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ IsAllowedVideoUrl()

bool FamiliesAutoWriter.ViewModels.WriterSession.IsAllowedVideoUrl ( string url)
inlinestaticprivate

Is Allowed Video Url 조건을 확인합니다.

매개변수
urlurl에 사용할 string 값입니다.
반환값
Is Allowed Video Url 조건이 충족되면 true이고, 그렇지 않으면 false입니다.

WriterSession.cs 파일의 1920 번째 라인에서 정의되었습니다.

1921 {
1922 if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return false;
1923 if (!url.Contains("youtube.com", StringComparison.OrdinalIgnoreCase) &&
1924 !url.Contains("youtu.be/", StringComparison.OrdinalIgnoreCase))
1925 return true;
1926
1927 // 일반 watch/shorts 링크는 소유자 설정으로 앱 내 iframe 재생이 자주 막힌다.
1928 return url.Contains("/embed/", StringComparison.OrdinalIgnoreCase);
1929 }

다음에 의해서 참조됨 : ParseTravelResponse().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ IsLikelyDirectImageUrl()

bool FamiliesAutoWriter.ViewModels.WriterSession.IsLikelyDirectImageUrl ( string url)
inlinestaticprivate

Is Likely Direct Image Url 조건을 확인합니다.

매개변수
urlurl에 사용할 string 값입니다.
반환값
Is Likely Direct Image Url 조건이 충족되면 true이고, 그렇지 않으면 false입니다.

WriterSession.cs 파일의 2444 번째 라인에서 정의되었습니다.

2445 {
2446 if (!IsPlausibleImageUrl(url) || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
2447 return false;
2448
2449 var path = uri.AbsolutePath;
2450 if (Regex.IsMatch(path, @"\.(jpe?g|png|webp|gif|bmp)(/)?$", RegexOptions.IgnoreCase))
2451 return true;
2452
2453 var host = uri.Host.ToLowerInvariant();
2454 return host.Contains("upload.wikimedia.org", StringComparison.Ordinal) ||
2455 host.Contains("images.unsplash.com", StringComparison.Ordinal) ||
2456 host.Contains("images.pexels.com", StringComparison.Ordinal) ||
2457 host.Contains("cdn.imweb.me", StringComparison.Ordinal);
2458 }

다음을 참조함 : IsPlausibleImageUrl().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:

◆ IsPlausibleImageUrl()

bool FamiliesAutoWriter.ViewModels.WriterSession.IsPlausibleImageUrl ( string url)
inlinestaticprivate

Is Plausible Image Url 조건을 확인합니다.

매개변수
urlurl에 사용할 string 값입니다.
반환값
Is Plausible Image Url 조건이 충족되면 true이고, 그렇지 않으면 false입니다.

WriterSession.cs 파일의 2402 번째 라인에서 정의되었습니다.

2403 {
2404 if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
2405 return false;
2406 if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
2407 return false;
2408
2409 var value = url.Trim();
2410 if (value.Contains("...", StringComparison.Ordinal) ||
2411 value.Contains("{", StringComparison.Ordinal) ||
2412 value.Contains("}", StringComparison.Ordinal) ||
2413 value.Contains("[", StringComparison.Ordinal) ||
2414 value.Contains("]", StringComparison.Ordinal))
2415 return false;
2416
2417 return true;
2418 }

다음에 의해서 참조됨 : IsLikelyDirectImageUrl(), ParseCookingResponseAsync(), ParseTravelResponse(), ValidateImageUrlsAsync().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ IsReachableImageAsync()

async Task< bool > FamiliesAutoWriter.ViewModels.WriterSession.IsReachableImageAsync ( string url,
HttpMethod method )
inlinestaticprivate

Is Reachable Image Async 조건을 확인합니다.

매개변수
urlurl에 사용할 string 값입니다.
methodmethod에 사용할 HttpMethod 값입니다.
반환값
Is Reachable Image Async 작업에서 생성한 Task<bool> 결과입니다.

WriterSession.cs 파일의 2570 번째 라인에서 정의되었습니다.

2571 {
2572 try
2573 {
2574 using var req = new HttpRequestMessage(method, url);
2575 req.Headers.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
2576 using var res = await _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
2577 if (!res.IsSuccessStatusCode) return false;
2578 // Content-Type이 있으면 image/* 인지 확인, 없으면 2xx 성공만으로 통과
2579 var ct = res.Content.Headers.ContentType?.MediaType;
2580 return ct == null || ct.StartsWith("image/", StringComparison.OrdinalIgnoreCase);
2581 }
2582 catch
2583 {
2584 return false;
2585 }
2586 }

다음을 참조함 : _http.

다음에 의해서 참조됨 : ValidateImageUrlsAsync().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ LoopWorkerAsync()

async Task FamiliesAutoWriter.ViewModels.WriterSession.LoopWorkerAsync ( CancellationToken ct)
inlineprivate

Loop Worker Async 작업을 수행합니다.

매개변수
ct취소 요청을 감시하는 토큰입니다.
반환값
Loop Worker Async 작업에서 생성한 Task 결과입니다.

WriterSession.cs 파일의 765 번째 라인에서 정의되었습니다.

766 {
767 try
768 {
769 while (!ct.IsCancellationRequested)
770 {
771 // 카운트다운
772 try
773 {
774 while (!ct.IsCancellationRequested && DateTime.Now < _nextLoopAt)
775 {
776 var r = _nextLoopAt - DateTime.Now;
777 if (r > TimeSpan.Zero)
778 {
779 var text = r.TotalHours >= 1
780 ? $"다음 {(int)r.TotalHours}:{r.Minutes:D2}:{r.Seconds:D2}"
781 : $"다음 {r.Minutes:D2}:{r.Seconds:D2}";
782 await _uiDispatcher.InvokeAsync(() => LoopCountdown = text);
783 }
784 await Task.Delay(1000, ct).ConfigureAwait(false);
785 }
786 }
787 catch (OperationCanceledException) { break; }
788
789 if (ct.IsCancellationRequested) break;
790
791 try
792 {
793 await RunLoopCycleAsync(ct).ConfigureAwait(false);
794 }
795 catch (OperationCanceledException) { break; }
796 catch (Exception ex)
797 {
798 await _uiDispatcher.InvokeAsync(() => LoopStatus = $"❌ 루프 오류: {ex.Message}");
799 await Task.Delay(3000, ct).ConfigureAwait(false);
800 }
801
802 if (!ct.IsCancellationRequested)
803 _nextLoopAt = DateTime.Now.AddMinutes(IntervalMinutes);
804 }
805 }
806 catch (OperationCanceledException) { }
807 catch (Exception ex)
808 {
809 await _uiDispatcher.InvokeAsync(() => LoopStatus = $"❌ 루프 중단: {ex.Message}");
810 }
811 finally
812 {
813 await _uiDispatcher.InvokeAsync(() => LoopCountdown = "");
814 }
815 }

다음을 참조함 : _nextLoopAt, _uiDispatcher, IntervalMinutes, RunLoopCycleAsync().

다음에 의해서 참조됨 : StartLoop().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ MergeUrls()

List< string > FamiliesAutoWriter.ViewModels.WriterSession.MergeUrls ( IEnumerable< string > primary,
IEnumerable< string > secondary )
inlinestaticprivate

Merge Urls 작업을 수행합니다.

매개변수
primaryprimary에 사용할 IEnumerable<string> 값입니다.
secondarysecondary에 사용할 IEnumerable<string> 값입니다.
반환값
Merge Urls 작업에서 생성한 List<string> 결과입니다.

WriterSession.cs 파일의 1840 번째 라인에서 정의되었습니다.

1841 {
1842 var merged = new List<string>();
1843 foreach (var url in primary.Concat(secondary))
1844 {
1845 var clean = url.Trim();
1846 if (clean.Length == 0) continue;
1847 if (!merged.Contains(clean, StringComparer.OrdinalIgnoreCase))
1848 merged.Add(clean);
1849 }
1850 return merged;
1851 }

다음에 의해서 참조됨 : ParseCookingResponseAsync(), ParseTravelResponse().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ NormalizeCookingContent()

string FamiliesAutoWriter.ViewModels.WriterSession.NormalizeCookingContent ( string content)
inlinestaticprivate

Normalize Cooking Content 작업을 수행합니다.

매개변수
contentcontent에 사용할 string 값입니다.
반환값
Normalize Cooking Content 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 1955 번째 라인에서 정의되었습니다.

1956 {
1957 var text = Regex.Replace(
1958 content,
1959 @"<img\b[^>]*>",
1960 "",
1961 RegexOptions.IgnoreCase);
1962
1963 text = NormalizeKnownHeadings(text, new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
1964 {
1965 ["오늘의 아기 밥"] = "## 오늘의 아기 밥",
1966 ["오늘의 가족 집밥"] = "## 오늘의 가족 집밥",
1967 ["만드는 법"] = "## 만드는 법",
1968 ["재료: 어른 2~3인분"] = "### 재료: 어른 2~3인분",
1969 ["재료: 어른 2-3인분"] = "### 재료: 어른 2~3인분",
1970 ["손질하기"] = "### 손질하기",
1971 ["양념에 재우기"] = "### 양념에 재우기",
1972 ["볶기"] = "### 볶기",
1973 ["끓이기"] = "### 끓이기",
1974 ["조리기"] = "### 조리기",
1975 ["부치기"] = "### 부치기",
1976 ["초보자 메모"] = "### 초보자 메모",
1977 ["별점"] = "## 별점",
1978 ["파워블로그 한줄평"] = "## 파워블로그 한줄평",
1979 });
1980
1981 return NormalizeImageBlockSpacing(NormalizeIngredientLines(text)).Trim();
1982 }

다음을 참조함 : NormalizeImageBlockSpacing(), NormalizeIngredientLines(), NormalizeKnownHeadings().

다음에 의해서 참조됨 : ParseCookingResponseAsync().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ NormalizeCookingTitle()

string FamiliesAutoWriter.ViewModels.WriterSession.NormalizeCookingTitle ( string title,
int number,
string mealName )
inlinestaticprivate

Normalize Cooking Title 작업을 수행합니다.

매개변수
titletitle에 사용할 string 값입니다.
numbernumber에 사용할 int 값입니다.
mealNamemeal Name에 사용할 string 값입니다.
반환값
Normalize Cooking Title 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 1668 번째 라인에서 정의되었습니다.

1669 {
1670 var normalized = string.IsNullOrWhiteSpace(title)
1671 ? $"[1983 집밥육아 #{number}] {mealName} 집밥 기록"
1672 : title.Trim();
1673
1674 normalized = Regex.Replace(normalized, @"#\d+|#번호\d+|#새숫자", $"#{number}", RegexOptions.IgnoreCase);
1675 normalized = normalized
1676 .Replace("[번호1]", number.ToString(), StringComparison.OrdinalIgnoreCase)
1677 .Replace("[번호2]", number.ToString(), StringComparison.OrdinalIgnoreCase)
1678 .Replace("[번호3]", number.ToString(), StringComparison.OrdinalIgnoreCase)
1679 .Replace("[새숫자]", number.ToString(), StringComparison.OrdinalIgnoreCase);
1680
1681 if (!Regex.IsMatch(normalized, @"#\d+"))
1682 normalized = $"[1983 집밥육아 #{number}] {normalized}";
1683
1684 normalized = Regex.IsMatch(normalized, @"아침|점심|저녁")
1685 ? Regex.Replace(normalized, @"아침|점심|저녁", mealName, RegexOptions.None, TimeSpan.FromMilliseconds(100))
1686 : Regex.Replace(normalized, @"(\‍]\s*)", "$1" + mealName + " ", RegexOptions.None, TimeSpan.FromMilliseconds(100));
1687
1688 return normalized;
1689 }

다음에 의해서 참조됨 : ParseCookingResponseAsync().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ NormalizeImageBlockSpacing()

string FamiliesAutoWriter.ViewModels.WriterSession.NormalizeImageBlockSpacing ( string content)
inlinestaticprivate

Normalize Image Block Spacing 작업을 수행합니다.

매개변수
contentcontent에 사용할 string 값입니다.
반환값
Normalize Image Block Spacing 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 2129 번째 라인에서 정의되었습니다.

2130 {
2131 var text = Regex.Replace(
2132 content.Replace("\r\n", "\n"),
2133 @"[ \t]*(<img\b)",
2134 "\n$1",
2135 RegexOptions.IgnoreCase);
2136 var lines = text.Split('\n');
2137 var normalized = new List<string>();
2138 var needsBlankBeforeNextContent = false;
2139
2140 foreach (var raw in lines)
2141 {
2142 var line = raw.TrimEnd();
2143 var isImage = line.TrimStart().StartsWith("<img", StringComparison.OrdinalIgnoreCase);
2144
2145 if (isImage)
2146 {
2147 AddBlankLineIfNeeded(normalized);
2148 normalized.Add(line.Trim());
2149 needsBlankBeforeNextContent = true;
2150 continue;
2151 }
2152
2153 if (needsBlankBeforeNextContent && line.Trim().Length > 0)
2154 {
2155 AddBlankLineIfNeeded(normalized);
2156 needsBlankBeforeNextContent = false;
2157 }
2158 else if (line.Trim().Length == 0)
2159 {
2160 needsBlankBeforeNextContent = false;
2161 }
2162
2163 normalized.Add(line);
2164 }
2165
2166 return string.Join("\n", normalized).Trim();
2167 }

다음을 참조함 : AddBlankLineIfNeeded().

다음에 의해서 참조됨 : NormalizeCookingContent(), NormalizeTravelContent().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ NormalizeIngredientLines()

string FamiliesAutoWriter.ViewModels.WriterSession.NormalizeIngredientLines ( string content)
inlinestaticprivate

Normalize Ingredient Lines 작업을 수행합니다.

매개변수
contentcontent에 사용할 string 값입니다.
반환값
Normalize Ingredient Lines 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 2313 번째 라인에서 정의되었습니다.

2314 {
2315 var lines = content.Replace("\r\n", "\n").Split('\n');
2316 var inIngredients = false;
2317 var builder = new StringBuilder();
2318
2319 foreach (var raw in lines)
2320 {
2321 var line = raw;
2322 var trimmed = line.Trim();
2323
2324 if (trimmed.StartsWith("### 재료:", StringComparison.Ordinal))
2325 {
2326 inIngredients = true;
2327 builder.AppendLine(line);
2328 continue;
2329 }
2330
2331 if (inIngredients && trimmed.StartsWith("### ", StringComparison.Ordinal))
2332 inIngredients = false;
2333
2334 if (inIngredients &&
2335 trimmed.Length > 0 &&
2336 !trimmed.StartsWith("-", StringComparison.Ordinal) &&
2337 Regex.IsMatch(trimmed, @"\d+(\.\d+)?\s*(g|kg|ml|l|개|큰술|작은술|컵|cm)\b", RegexOptions.IgnoreCase))
2338 {
2339 line = "- " + trimmed;
2340 }
2341
2342 builder.AppendLine(line);
2343 }
2344
2345 return builder.ToString().TrimEnd();
2346 }

다음에 의해서 참조됨 : NormalizeCookingContent().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ NormalizeKnownHeadings()

string FamiliesAutoWriter.ViewModels.WriterSession.NormalizeKnownHeadings ( string content,
IReadOnlyDictionary< string, string > headings )
inlinestaticprivate

Normalize Known Headings 작업을 수행합니다.

매개변수
contentcontent에 사용할 string 값입니다.
headingsheadings에 사용할 IReadOnlyDictionary<string, string> 값입니다.
반환값
Normalize Known Headings 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 2016 번째 라인에서 정의되었습니다.

2017 {
2018 var lines = content.Replace("\r\n", "\n").Split('\n');
2019 for (var i = 0; i < lines.Length; i++)
2020 {
2021 var trimmed = lines[i].Trim();
2022 if (trimmed.StartsWith("#", StringComparison.Ordinal) ||
2023 trimmed.StartsWith("<", StringComparison.Ordinal) ||
2024 trimmed.Length == 0)
2025 continue;
2026
2027 if (headings.TryGetValue(trimmed, out var heading))
2028 lines[i] = heading;
2029 }
2030
2031 return string.Join("\n", lines);
2032 }

다음에 의해서 참조됨 : NormalizeCookingContent(), NormalizeTravelContent().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ NormalizeSimpleTables()

string FamiliesAutoWriter.ViewModels.WriterSession.NormalizeSimpleTables ( string content)
inlinestaticprivate

Normalize Simple Tables 작업을 수행합니다.

매개변수
contentcontent에 사용할 string 값입니다.
반환값
Normalize Simple Tables 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 2058 번째 라인에서 정의되었습니다.

2059 {
2060 var lines = content.Replace("\r\n", "\n").Split('\n').ToList();
2061 for (var i = 0; i < lines.Count; i++)
2062 {
2063 var expanded = TryExpandCollapsedPipeTable(lines[i]);
2064 if (expanded.Count > 0)
2065 {
2066 if (i > 0 && !string.IsNullOrWhiteSpace(lines[i - 1]) && expanded[0].Length > 0)
2067 expanded.Insert(0, "");
2068 if (i + 1 < lines.Count && !string.IsNullOrWhiteSpace(lines[i + 1]) && expanded[^1].Length > 0)
2069 expanded.Add("");
2070 lines.RemoveAt(i);
2071 lines.InsertRange(i, expanded);
2072 i += expanded.Count - 1;
2073 continue;
2074 }
2075
2076 var cols = SplitTableLine(lines[i]);
2077 if (cols.Count < 3 || !cols.Any(c => c.Contains("비용", StringComparison.OrdinalIgnoreCase))) continue;
2078
2079 var rows = new List<List<string>> { cols };
2080 var j = i + 1;
2081 while (j < lines.Count)
2082 {
2083 var row = SplitTableLine(lines[j]);
2084 if (row.Count != cols.Count) break;
2085 rows.Add(row);
2086 j++;
2087 }
2088
2089 if (rows.Count < 2) continue;
2090
2091 var md = new List<string>
2092 {
2093 "| " + string.Join(" | ", rows[0]) + " |",
2094 "| " + string.Join(" | ", rows[0].Select((_, idx) => idx == 1 ? "---:" : "---")) + " |"
2095 };
2096 md.AddRange(rows.Skip(1).Select(row => "| " + string.Join(" | ", row) + " |"));
2097 lines.RemoveRange(i, rows.Count);
2098 lines.InsertRange(i, md);
2099 i += md.Count - 1;
2100 }
2101
2102 return string.Join("\n", lines);
2103 }

다음을 참조함 : SplitTableLine(), TryExpandCollapsedPipeTable().

다음에 의해서 참조됨 : NormalizeTravelContent().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ NormalizeTravelContent()

string FamiliesAutoWriter.ViewModels.WriterSession.NormalizeTravelContent ( string content)
inlinestaticprivate

Normalize Travel Content 작업을 수행합니다.

매개변수
contentcontent에 사용할 string 값입니다.
반환값
Normalize Travel Content 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 1877 번째 라인에서 정의되었습니다.

1878 {
1879 var text = NormalizeKnownHeadings(content, new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
1880 {
1881 ["한눈에 보는 포인트"] = "## 한눈에 보는 포인트",
1882 ["이런 가족에게 추천"] = "## 이런 가족에게 추천",
1883 ["아이와 좋았던 점"] = "## 아이와 좋았던 점",
1884 ["동선과 시간표"] = "## 동선과 시간표",
1885 ["여행 경비 예상"] = "## 여행 경비 예상",
1886 ["주차·화장실·유모차 포인트"] = "## 주차·화장실·유모차 포인트",
1887 ["주차・화장실・유모차 포인트"] = "## 주차·화장실·유모차 포인트",
1888 ["준비물과 주의할 것"] = "## 준비물과 주의할 것",
1889 ["방문 전 확인할 것"] = "## 방문 전 확인할 것",
1890 ["총평"] = "## 총평",
1891 });
1892
1893 return NormalizeImageBlockSpacing(NormalizeSimpleTables(text));
1894 }

다음을 참조함 : NormalizeImageBlockSpacing(), NormalizeKnownHeadings(), NormalizeSimpleTables().

다음에 의해서 참조됨 : ExtractAndSaveAsync(), ParseTravelResponse(), RunLoopCycleAsync().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ ParseCookingResponseAsync()

async Task< List< PostEntry > > FamiliesAutoWriter.ViewModels.WriterSession.ParseCookingResponseAsync ( string? raw)
inlineprivate

Parse Cooking Response Async 작업을 수행합니다.

매개변수
rawraw에 사용할 string? 값입니다.
반환값
Parse Cooking Response Async 작업에서 생성한 Task<List<PostEntry>> 결과입니다.

WriterSession.cs 파일의 1479 번째 라인에서 정의되었습니다.

1480 {
1481 var text = UnwrapRaw(raw);
1482 if (string.IsNullOrWhiteSpace(text)) return [];
1483
1484 // IMAGES 섹션에서 이미지 URL 목록 — 실제 존재하는 것만 사용
1485 var rawImageUrls = GetSection(text, "IMAGES")
1486 .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
1487 .Where(l => l.StartsWith("http", StringComparison.OrdinalIgnoreCase))
1488 .Where(IsPlausibleImageUrl)
1489 .ToList();
1490 // 이미 사용한 이미지 URL 제외하여 중복 이미지 방지
1491 var validImages = rawImageUrls.Count > 0
1492 ? await ValidateImageUrlsAsync(rawImageUrls, null, _usedImageUrls)
1493 : [];
1494 // 요리: 포스트당 최대 2개 사용 — 검증 통과한 전체 풀에서 순서대로 배분
1495 const int CookingImagesPerPost = 2;
1496 var imageUrls = validImages.ToArray();
1497
1498 var posts = new List<PostEntry>();
1499 var timeline = _writer.GetCookingTimelineState(SelectedSlug);
1500 var nextPostedAt = GetNextCookingSlot(timeline.LastPostedAt);
1501 var nextNumber = timeline.LastNumber > 0 ? timeline.LastNumber + 1 : 1;
1502
1503 // ===DETAIL_NNN_TITLE=== 패턴으로 포스트 수 감지
1504 // AI가 [번호1], [새숫자] 같은 자리표시자를 남겨도 가능한 범위에서 복구한다.
1505 var detailNums = Regex
1506 .Matches(text, @"===DETAIL_(?<num>\d+|\‍[[^\‍]]+\‍])_TITLE===", RegexOptions.IgnoreCase)
1507 .Select(m => m.Groups["num"].Value)
1508 .Distinct()
1509 .OrderBy(n => int.TryParse(n, out var i) ? i : int.MaxValue)
1510 .ThenBy(n => n)
1511 .ToList();
1512
1513 int imgIdx = 0;
1514 foreach (var num in detailNums)
1515 {
1516 var title = GetSection(text, $"DETAIL_{num}_TITLE");
1517 var date = GetSection(text, $"DETAIL_{num}_DATE");
1518 var content = GetSection(text, $"DETAIL_{num}_CONTENT");
1519
1520 if (string.IsNullOrWhiteSpace(content)) continue;
1521 var displayNum = int.TryParse(num, out _) ? num : $"{DateTime.Now:HHmm}{posts.Count + 1}";
1522 var postedAt = nextPostedAt;
1523 var mealName = GetCookingMealName(postedAt);
1524 title = NormalizeCookingTitle(title, nextNumber, mealName);
1525
1526 // 이 포스트에 해당하는 이미지 — 검증 풀에서 최대 CookingImagesPerPost개 순서대로
1527 var postImages = imageUrls
1528 .Skip(imgIdx)
1529 .Take(CookingImagesPerPost)
1530 .ToList();
1531 imgIdx += CookingImagesPerPost;
1532
1533 var substitutedSource = SubstituteImages(content, postImages);
1534 var bodyImages = ExtractImageUrls(substitutedSource);
1535 var substituted = NormalizeCookingContent(substitutedSource);
1536
1537 // 사용된 이미지 URL 추적 (다음 사이클에서 중복 방지)
1538 foreach (var img in postImages) _usedImageUrls.Add(img);
1539
1540 posts.Add(new PostEntry
1541 {
1542 Title = string.IsNullOrWhiteSpace(title) ? $"[1983 집밥육아 #{nextNumber}] {mealName} 집밥 기록" : title,
1543 Content = substituted,
1544 AlbumId = SelectedAlbum?.Id ?? "",
1545 PostedAt = postedAt,
1547 PhotoFileNames = MergeUrls(postImages, bodyImages),
1548 VideoFileNames = [],
1549 });
1550
1551 nextPostedAt = GetNextCookingSlot(postedAt);
1552 nextNumber++;
1553 }
1554
1555 return posts;
1556 }

다음을 참조함 : _usedImageUrls, _writer, ExtractImageUrls(), GetCookingMealName(), GetNextCookingSlot(), GetSection(), IsPlausibleImageUrl(), MergeUrls(), NormalizeCookingContent(), NormalizeCookingTitle(), SelectedAlbum, SelectedSlug, SubstituteImages(), UnwrapRaw(), ValidateImageUrlsAsync().

다음에 의해서 참조됨 : ExtractAndSaveAsync(), RunLoopCycleAsync().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ ParseTravelResponse()

? AiPostResult FamiliesAutoWriter.ViewModels.WriterSession.ParseTravelResponse ( string? raw)
inlinestaticprivate

Parse Travel Response 작업을 수행합니다.

매개변수
rawraw에 사용할 string? 값입니다.
반환값
Parse Travel Response 작업에서 생성한 AiPostResult? 결과입니다.

WriterSession.cs 파일의 1416 번째 라인에서 정의되었습니다.

1417 {
1418 var text = UnwrapRaw(raw);
1419 if (string.IsNullOrWhiteSpace(text)) return null;
1420
1421 var content = GetSection(text, "CONTENT");
1422 if (string.IsNullOrWhiteSpace(content))
1423 {
1424 // ===CONTENT=== 마커를 AI가 빠뜨린 경우 — ===TITLE=== 다음 줄 ~ ===IMAGES=== 앞 텍스트로 fallback
1425 var titleEnd = text.IndexOf("===TITLE===", StringComparison.OrdinalIgnoreCase);
1426 var imagesIdx = text.IndexOf("===IMAGES===", StringComparison.OrdinalIgnoreCase);
1427 if (titleEnd >= 0 && imagesIdx > titleEnd)
1428 {
1429 var afterTitle = text.IndexOf('\n', titleEnd + "===TITLE===".Length);
1430 if (afterTitle >= 0)
1431 {
1432 var afterTitleLine = text.IndexOf('\n', afterTitle + 1);
1433 if (afterTitleLine >= 0 && afterTitleLine < imagesIdx)
1434 content = text[afterTitleLine..imagesIdx].Trim();
1435 }
1436 }
1437 if (string.IsNullOrWhiteSpace(content)) return null;
1438 }
1439
1440 var photos = GetSection(text, "IMAGES")
1441 .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
1442 .Where(l => l.StartsWith("http", StringComparison.OrdinalIgnoreCase))
1443 .Where(IsPlausibleImageUrl)
1444 .ToList();
1445 var videos = GetSection(text, "VIDEO")
1446 .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
1447 .Where(IsAllowedVideoUrl).ToList();
1448
1449 content = NormalizeTravelContent(SubstituteImages(content, photos));
1450 photos = MergeUrls(photos, ExtractImageUrls(content));
1451 return new AiPostResult(GetSection(text, "TITLE"), content, photos, videos);
1452 }

다음을 참조함 : AiPostResult(), ExtractImageUrls(), GetSection(), IsAllowedVideoUrl(), IsPlausibleImageUrl(), MergeUrls(), NormalizeTravelContent(), SubstituteImages(), UnwrapRaw().

다음에 의해서 참조됨 : ExtractAndSaveAsync(), RunLoopCycleAsync().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ RefreshAlbums()

void FamiliesAutoWriter.ViewModels.WriterSession.RefreshAlbums ( )
inlineprivate

Refresh Albums 작업을 수행합니다.

WriterSession.cs 파일의 648 번째 라인에서 정의되었습니다.

649 {
650 Albums.Clear();
651 if (string.IsNullOrWhiteSpace(SelectedSlug)) return;
652 foreach (var a in _writer.GetAlbums(SelectedSlug)) Albums.Add(a);
653 _albumIndex = 0;
654 SelectedAlbum = Albums.Count > 0 ? Albums[0] : null;
655 RefreshPromptWithExisting();
656 }

다음을 참조함 : _albumIndex, _writer, Albums, RefreshPromptWithExisting(), SelectedAlbum, SelectedSlug.

다음에 의해서 참조됨 : RefreshSlugList().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ RefreshPromptWithExisting()

void FamiliesAutoWriter.ViewModels.WriterSession.RefreshPromptWithExisting ( )
inlineprivate

Refresh Prompt With Existing 작업을 수행합니다.

WriterSession.cs 파일의 666 번째 라인에서 정의되었습니다.

667 {
668 if (string.IsNullOrWhiteSpace(SelectedSlug)) return;
669 var existing = _writer.GetExistingTitles(SelectedSlug);
670 PromptText = Mode == SessionMode.Cooking
671 ? BuildCookingPrompt(existing, _writer.GetCookingTimelineState(SelectedSlug))
672 : BuildTravelPrompt(existing);
673 }

다음을 참조함 : _writer, BuildCookingPrompt(), BuildTravelPrompt(), FamiliesAutoWriter.ViewModels.Cooking, Mode, SelectedSlug.

다음에 의해서 참조됨 : RefreshAlbums(), RunLoopCycleAsync().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ RefreshSlugList()

void FamiliesAutoWriter.ViewModels.WriterSession.RefreshSlugList ( )
inlineprivate

Refresh Slug List 작업을 수행합니다.

WriterSession.cs 파일의 571 번째 라인에서 정의되었습니다.

571{ RefreshSlugs(); RefreshAlbums(); }

다음을 참조함 : RefreshAlbums(), RefreshSlugs().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:

◆ RefreshSlugs()

void FamiliesAutoWriter.ViewModels.WriterSession.RefreshSlugs ( )
inlineprivate

Refresh Slugs 작업을 수행합니다.

WriterSession.cs 파일의 631 번째 라인에서 정의되었습니다.

632 {
633 _writer.AppDataRoot = AppDataRoot;
634 Slugs.Clear();
635 foreach (var s in _writer.GetSlugs()) Slugs.Add(s);
636 if (Slugs.Count > 0 && string.IsNullOrEmpty(SelectedSlug))
637 SelectedSlug = Slugs[0];
638 }

다음을 참조함 : _writer, AppDataRoot, SelectedSlug, Slugs.

다음에 의해서 참조됨 : RefreshSlugList(), WriterSession().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ RemoveInvalidInlineImages()

string FamiliesAutoWriter.ViewModels.WriterSession.RemoveInvalidInlineImages ( string content,
IReadOnlyCollection< string > allowedUrls )
inlinestaticprivate

Invalid Inline Images 항목을 제거합니다.

매개변수
contentcontent에 사용할 string 값입니다.
allowedUrlsallowed Urls에 사용할 IReadOnlyCollection<string> 값입니다.
반환값
Remove Invalid Inline Images 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 1783 번째 라인에서 정의되었습니다.

1784 {
1785 if (string.IsNullOrWhiteSpace(content))
1786 return content;
1787
1788 var allowed = allowedUrls
1789 .Select(u => System.Net.WebUtility.HtmlDecode(u).Trim())
1790 .Where(u => u.Length > 0)
1791 .ToHashSet(StringComparer.OrdinalIgnoreCase);
1792
1793 return Regex.Replace(
1794 content,
1795 @"<img\b[^>]*>",
1796 match =>
1797 {
1798 var src = Regex.Match(match.Value, @"\bsrc\s*=\s*[""'](?<url>https?://[^""']+)[""']", RegexOptions.IgnoreCase);
1799 if (!src.Success)
1800 return "";
1801
1802 var url = System.Net.WebUtility.HtmlDecode(src.Groups["url"].Value.Trim());
1803 return allowed.Contains(url) ? match.Value : ""; // 검증 통과 URL만 유지
1804 },
1805 RegexOptions.IgnoreCase);
1806 }

다음에 의해서 참조됨 : ExtractAndSaveAsync(), RunLoopCycleAsync().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ RotateAlbum()

void FamiliesAutoWriter.ViewModels.WriterSession.RotateAlbum ( )
inlineprivate

Rotate Album 작업을 수행합니다.

WriterSession.cs 파일의 1087 번째 라인에서 정의되었습니다.

1088 {
1089 if (Albums.Count == 0) return;
1090 _albumIndex = (_albumIndex + 1) % Albums.Count;
1091 SelectedAlbum = Albums[_albumIndex];
1092 }

다음을 참조함 : _albumIndex, Albums, SelectedAlbum.

다음에 의해서 참조됨 : RunLoopCycleAsync().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ RunLoopCycleAsync()

async Task FamiliesAutoWriter.ViewModels.WriterSession.RunLoopCycleAsync ( CancellationToken ct)
inlineprivate

Run Loop Cycle Async 작업을 수행합니다.

매개변수
ct취소 요청을 감시하는 토큰입니다.
반환값
Run Loop Cycle Async 작업에서 생성한 Task 결과입니다.

WriterSession.cs 파일의 842 번째 라인에서 정의되었습니다.

843 {
844 // 상태 읽기는 UI 스레드에서
845 var (slug, album, prompt, execFn) = await _uiDispatcher.InvokeAsync(() =>
846 (SelectedSlug, SelectedAlbum, PromptText, ExecuteScriptAsync));
847
848 if (string.IsNullOrWhiteSpace(slug)) { await SetStatus("❌ 슬러그를 선택하세요."); return; }
849 if (string.IsNullOrWhiteSpace(prompt)) { await SetStatus("❌ 프롬프트를 입력하세요."); return; }
850 if (execFn == null) { await SetStatus("❌ 브라우저 준비 안 됨"); return; }
851
852 var albumName = album?.Name ?? "전체 타임라인";
853 var filled = prompt.Replace("{앨범}", albumName).Replace("{album}", albumName);
854
855
856 try
857 {
858 // 전송 전 현재 마지막 응답을 베이스라인으로 캡처 — 이전 응답 오탐 방지
859 var baselineRaw = await await _uiDispatcher.InvokeAsync(() => execFn(ResponseExtractor.BuildExtractScript()));
860 string baselineText = "";
861 try
862 {
863 using var bd = System.Text.Json.JsonDocument.Parse(baselineRaw ?? "{}");
864 baselineText = bd.RootElement.TryGetProperty("text", out var bt) ? bt.GetString() ?? "" : "";
865 }
866 catch { }
867
868 await SetStatus($"📤 [{albumName}] 전송 중...");
869 await await _uiDispatcher.InvokeAsync(() => execFn(PromptInjector.BuildInjectScript(filled)));
870
871 // AI 시작 대기 (10초)
872 await SetStatus($"⏳ [{albumName}] AI 시작 대기...");
873 await Task.Delay(10_000, ct).ConfigureAwait(false);
874
875 // 완료 판정:
876 // 1순위 — ===VIDEO=== 발견 시 즉시 완료 (completed 플래그)
877 // 2순위 — 종료 마커가 있는 텍스트 안정화: 15초 간격으로 2회 연속 동일하면 완료
878 string? raw = null;
879 string prevText = "";
880 int stableCount = 0;
881 for (int retry = 0; retry <= 60; retry++) // 최대 15분
882 {
883 if (ct.IsCancellationRequested) return;
884
885 var extracted = await await _uiDispatcher.InvokeAsync(() => execFn(ResponseExtractor.BuildExtractScript()));
886 string curText = "";
887 bool completed = false;
888 try
889 {
890 using var doc = System.Text.Json.JsonDocument.Parse(extracted ?? "{}");
891 curText = doc.RootElement.TryGetProperty("text", out var t) ? t.GetString() ?? "" : "";
892 completed = doc.RootElement.TryGetProperty("completed", out var c) && c.GetBoolean();
893 }
894 catch { curText = extracted ?? ""; }
895
896 // 이전 응답(베이스라인)과 동일하면 아직 새 응답이 아님 → 스킵
897 bool isNewResponse = curText.Length > 20 && curText != baselineText;
898
899 // 1순위: 종료 마커 확인 (새 응답일 때만)
900 if (completed && isNewResponse)
901 {
902 raw = extracted;
903 break;
904 }
905
906 // 2순위: 텍스트 안정화 (새 응답일 때만)
907 var hasEndMarker = curText.Contains("===VIDEO===", StringComparison.OrdinalIgnoreCase);
908 if (isNewResponse && hasEndMarker && curText == prevText)
909 {
910 stableCount++;
911 if (stableCount >= 2)
912 {
913 raw = extracted;
914 break;
915 }
916 }
917 else
918 {
919 stableCount = 0;
920 }
921 prevText = curText;
922
923 var elapsed = 8 + retry * 15;
924 await SetStatus($"⏳ [{albumName}] AI 생성 중... ({elapsed}s)");
925 await Task.Delay(15_000, ct).ConfigureAwait(false);
926 }
927
928 if (raw == null)
929 {
930 await SetStatus("⏱ AI 응답 대기 시간 초과 (응답 없음)");
931 return;
932 }
933
934 var mode = await _uiDispatcher.InvokeAsync(() => Mode);
935 var mediaPos = await _uiDispatcher.InvokeAsync(() => MediaPosition);
936 var albumId = album?.Id ?? "";
937 var newChatN = await _uiDispatcher.InvokeAsync(() => NewChatEveryN);
938 var rotEnabled = await _uiDispatcher.InvokeAsync(() => AlbumRotationEnabled);
939
940 for (var extractAttempt = 1; extractAttempt <= ExtractionRetryMaxAttempts; extractAttempt++)
941 {
942 if (mode == SessionMode.Cooking)
943 {
944 var posts = await ParseCookingResponseAsync(raw);
945 if (posts.Count == 0)
946 {
947 if (extractAttempt >= ExtractionRetryMaxAttempts)
948 {
949 await SetStatus("❌ 요리 포맷 추출 실패 (===DETAIL_NNN=== 섹션 없음)");
950 return;
951 }
952
953 await SetStatus($"⏳ [{albumName}] AI 응답 불완전 - {ExtractionRetryDelaySeconds}초 후 재추출 ({extractAttempt}/{ExtractionRetryMaxAttempts})");
954 await Task.Delay(TimeSpan.FromSeconds(ExtractionRetryDelaySeconds), ct).ConfigureAwait(false);
955 raw = await await _uiDispatcher.InvokeAsync(() => execFn(ResponseExtractor.BuildExtractScript()));
956 continue;
957 }
958
959 // 중복 제목 방지
960 var existingCookingTitles = _writer.GetExistingTitles(slug);
961 var existingCookingNumbers = existingCookingTitles
962 .Select(ExtractCookingNumberFromTitle)
963 .Where(n => n > 0)
964 .ToHashSet();
965 var newPosts = posts.Where(p =>
966 !existingCookingTitles.Any(t => string.Equals(t, p.Title, StringComparison.OrdinalIgnoreCase)) &&
967 !existingCookingNumbers.Contains(ExtractCookingNumberFromTitle(p.Title))
968 ).ToList();
969 if (newPosts.Count == 0) { await SetStatus($"⏭ [{albumName}] 모두 중복 제목 스킵"); return; }
970
971 foreach (var p in newPosts) await _writer.SavePostAsync(slug, p, []);
972 _cycleCount++;
973 await SetStatus($"✅ [{albumName}] #{_cycleCount} {newPosts.Count}개 저장! ({DateTime.Now:HH:mm})");
974 break;
975 }
976 else
977 {
978 var result = ParseTravelResponse(raw);
979 if (result is null || string.IsNullOrWhiteSpace(result.Content))
980 {
981 if (extractAttempt >= ExtractionRetryMaxAttempts)
982 {
983 await SetStatus("❌ 추출 실패 (===CONTENT=== 섹션 없음 — AI 응답 불완전)");
984 return;
985 }
986
987 await SetStatus($"⏳ [{albumName}] AI 응답 불완전 - {ExtractionRetryDelaySeconds}초 후 재추출 ({extractAttempt}/{ExtractionRetryMaxAttempts})");
988 await Task.Delay(TimeSpan.FromSeconds(ExtractionRetryDelaySeconds), ct).ConfigureAwait(false);
989 raw = await await _uiDispatcher.InvokeAsync(() => execFn(ResponseExtractor.BuildExtractScript()));
990 continue;
991 }
992
993 List<string> validPhotos = [];
994 if (result.Photos.Count > 0)
995 {
996 await SetStatus("🔍 이미지 검증 중...");
997 var allValid = await ValidateImageUrlsAsync(result.Photos, msg => _ = SetStatus(msg));
998 validPhotos = allValid.Take(3).ToList(); // 여행: 최대 3개 사용
999 }
1000 var content = NormalizeTravelContent(RemoveInvalidInlineImages(result.Content, validPhotos));
1001
1002 var postTitle = !string.IsNullOrWhiteSpace(result.Title) ? result.Title : $"[{albumName}] {DateTime.Now:MM/dd HH:mm}";
1003
1004 // 중복 제목 방지 — 같은 제목이 이미 저장됐으면 스킵
1005 var existingTitles = _writer.GetExistingTitles(slug);
1006 if (existingTitles.Any(t => string.Equals(t, postTitle, StringComparison.OrdinalIgnoreCase)))
1007 {
1008 await SetStatus($"⏭ [{albumName}] 중복 제목 스킵: {postTitle[..Math.Min(30, postTitle.Length)]}...");
1009 return;
1010 }
1011
1012 var post = new PostEntry
1013 {
1014 Title = postTitle,
1015 Content = content,
1016 AlbumId = albumId,
1017 PostedAt = DateTime.Now,
1018 MediaPosition = mediaPos,
1019 PhotoFileNames = validPhotos,
1020 VideoFileNames = result.Videos,
1021 };
1022 await _writer.SavePostAsync(slug, post, []);
1023 _cycleCount++;
1024 await SetStatus($"✅ [{albumName}] #{_cycleCount} 저장! ({DateTime.Now:HH:mm}) 📷{validPhotos.Count} 🎬{result.Videos.Count}");
1025 break;
1026 }
1027 }
1028
1029 // 저장 완료 후 프롬프트 갱신 — 방금 쓴 제목이 다음 사이클 "제외 목록"에 반영됨
1030 await _uiDispatcher.InvokeAsync(() => RefreshPromptWithExisting());
1031
1032 if (rotEnabled) await _uiDispatcher.InvokeAsync(() => RotateAlbum());
1033
1034 if (newChatN > 0 && _cycleCount % newChatN == 0)
1035 {
1036 await SetStatus("🔄 새 채팅으로 이동 중...");
1037 await Task.Delay(2000, ct).ConfigureAwait(false);
1038 await _uiDispatcher.InvokeAsync(() => NavigateTab?.Invoke("__new_chat__"));
1039 // 새 채팅 페이지 로딩 완료까지 충분히 대기
1040 for (int i = 15; i > 0; i--)
1041 {
1042 if (ct.IsCancellationRequested) return;
1043 await SetStatus($"🔄 새 채팅 로딩 대기 {i}초...");
1044 await Task.Delay(1000, ct).ConfigureAwait(false);
1045 }
1046 await SetStatus("✅ 새 채팅 준비 완료");
1047 }
1048 }
1049 catch (OperationCanceledException) { await SetStatus("⏹ 취소됨"); }
1050 catch (Exception ex) { await SetStatus($"❌ {ex.Message}"); }
1051 }

다음을 참조함 : _cycleCount, _uiDispatcher, _writer, FamiliesAutoWriter.Services.ResponseExtractor.BuildExtractScript(), FamiliesAutoWriter.Services.PromptInjector.BuildInjectScript(), ExecuteScriptAsync, ExtractCookingNumberFromTitle(), ExtractionRetryDelaySeconds, ExtractionRetryMaxAttempts, Mode, NavigateTab, NormalizeTravelContent(), ParseCookingResponseAsync(), ParseTravelResponse(), RefreshPromptWithExisting(), RemoveInvalidInlineImages(), RotateAlbum(), SelectedAlbum, SelectedSlug, SetStatus(), ValidateImageUrlsAsync().

다음에 의해서 참조됨 : LoopWorkerAsync().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ RunNow()

void FamiliesAutoWriter.ViewModels.WriterSession.RunNow ( )
inlineprivate

Run Now 작업을 수행합니다.

WriterSession.cs 파일의 580 번째 라인에서 정의되었습니다.

580=> _ = RunLoopCycleAsync(CancellationToken.None);

◆ SendPromptNow()

void FamiliesAutoWriter.ViewModels.WriterSession.SendPromptNow ( )
inlineprivate

Send Prompt Now 작업을 수행합니다.

WriterSession.cs 파일의 589 번째 라인에서 정의되었습니다.

589=> _ = SendPromptNowAsync();

◆ SendPromptNowAsync()

async Task FamiliesAutoWriter.ViewModels.WriterSession.SendPromptNowAsync ( )
inlineprivate

Send Prompt Now Async 작업을 수행합니다.

반환값
Send Prompt Now Async 작업에서 생성한 Task 결과입니다.

WriterSession.cs 파일의 1111 번째 라인에서 정의되었습니다.

1112 {
1113 if (string.IsNullOrWhiteSpace(PromptText)) { PromptStatus = "❌ 프롬프트를 입력하세요."; return; }
1114 if (ExecuteScriptAsync == null) { PromptStatus = "❌ 브라우저 준비 안 됨"; return; }
1115 var albumName = SelectedAlbum?.Name ?? "전체";
1116 var prompt = PromptText.Replace("{앨범}", albumName).Replace("{album}", albumName);
1117 await ExecuteScriptAsync(PromptInjector.BuildInjectScript(prompt));
1118 PromptStatus = $"✅ 전송 ({DateTime.Now:HH:mm})";
1119 }

다음을 참조함 : FamiliesAutoWriter.Services.PromptInjector.BuildInjectScript(), ExecuteScriptAsync, SelectedAlbum.

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:

◆ SetStatus()

Task FamiliesAutoWriter.ViewModels.WriterSession.SetStatus ( string msg)
inlineprivate

Status 값을 설정합니다.

매개변수
msgmsg에 사용할 string 값입니다.
반환값
Set Status 작업에서 생성한 Task 결과입니다.

WriterSession.cs 파일의 1077 번째 라인에서 정의되었습니다.

1077=> _uiDispatcher.InvokeAsync(() => LoopStatus = msg).Task;

다음을 참조함 : _uiDispatcher.

다음에 의해서 참조됨 : RunLoopCycleAsync().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ SplitTableLine()

List< string > FamiliesAutoWriter.ViewModels.WriterSession.SplitTableLine ( string line)
inlinestaticprivate

Split Table Line 작업을 수행합니다.

매개변수
lineline에 사용할 string 값입니다.
반환값
Split Table Line 작업에서 생성한 List<string> 결과입니다.

WriterSession.cs 파일의 2281 번째 라인에서 정의되었습니다.

2281 =>
2282 line.Contains('\t')
2283 ? line.Split('\t', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList()
2284 : Regex.Split(line.Trim(), @"\s{2,}")
2285 .Where(c => !string.IsNullOrWhiteSpace(c))
2286 .Select(c => c.Trim())
2287 .ToList();

다음에 의해서 참조됨 : NormalizeSimpleTables().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ StartLoop()

void FamiliesAutoWriter.ViewModels.WriterSession.StartLoop ( )
inlineprivate

Start Loop 작업을 수행합니다.

WriterSession.cs 파일의 694 번째 라인에서 정의되었습니다.

695 {
696 _loopCts?.Cancel();
697 _loopCts?.Dispose();
698 _loopCts = new CancellationTokenSource();
699 _nextLoopAt = DateTime.Now.AddMinutes(IntervalMinutes);
700 var ct = _loopCts.Token;
701 _loopTask = Task.Run(() => LoopWorkerAsync(ct));
702 var nc = NewChatEveryN > 0 ? $" | 🔄{NewChatEveryN}개" : "";
703 LoopStatus = $"✅ {SelectedInterval?.Label} | AI대기 {AiWaitOption?.Label}{nc}";
704 }

다음을 참조함 : _loopCts, _loopTask, _nextLoopAt, IntervalMinutes, LoopWorkerAsync().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:

◆ StopAutomation()

void FamiliesAutoWriter.ViewModels.WriterSession.StopAutomation ( )
inline

Stop Automation 작업을 수행합니다.

WriterSession.cs 파일의 732 번째 라인에서 정의되었습니다.

733 {
734 if (LoopEnabled)
735 LoopEnabled = false;
736 else
737 StopLoop();
738 }

다음을 참조함 : LoopEnabled, StopLoop().

다음에 의해서 참조됨 : FamiliesAutoWriter.MainWindow.CloseTab().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ StopLoop()

void FamiliesAutoWriter.ViewModels.WriterSession.StopLoop ( )
inlineprivate

Stop Loop 작업을 수행합니다.

WriterSession.cs 파일의 714 번째 라인에서 정의되었습니다.

715 {
716 _loopCts?.Cancel();
717 _loopCts?.Dispose();
718 _loopCts = null;
719 _nextLoopAt = DateTime.MaxValue;
720 LoopCountdown = "";
721 LoopStatus = "⏸ 루프 꺼짐";
722 }

다음을 참조함 : _loopCts, _nextLoopAt.

다음에 의해서 참조됨 : StopAutomation().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ SubstituteImages()

string FamiliesAutoWriter.ViewModels.WriterSession.SubstituteImages ( string content,
IList< string > urls,
int offset = 0 )
inlinestaticprivate

Substitute Images 작업을 수행합니다.

매개변수
contentcontent에 사용할 string 값입니다.
urlsurls에 사용할 IList<string> 값입니다.
offsetoffset에 사용할 int 값입니다.
반환값
Substitute Images 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 1361 번째 라인에서 정의되었습니다.

1362 {
1363 // 한국어 순서 표현 매핑
1364 string[] ordinals = ["첫 번째", "두 번째", "세 번째", "네 번째", "다섯 번째"];
1365 var result = content;
1366
1367 for (int i = 0; i < urls.Count; i++)
1368 {
1369 // "[IMAGES N번째 URL]" 형태 치환
1370 if (i < ordinals.Length)
1371 result = result.Replace($"[IMAGES {ordinals[i]} URL]", urls[i], StringComparison.OrdinalIgnoreCase);
1372 }
1373
1374 // offset 기반 치환: 요리 포스트별로 "첫 번째"가 실제로는 offset번째 이미지를 가리킴
1375 if (offset > 0 && offset < ordinals.Length && offset < urls.Count + offset)
1376 {
1377 // 이미 치환됐을 수 있으니 남은 [IMAGES 첫 번째 URL] 형태만 처리
1378 result = result.Replace("[IMAGES 첫 번째 URL]", urls.Count > 0 ? urls[0] : "", StringComparison.OrdinalIgnoreCase);
1379 }
1380
1381 // 치환 안 된 플레이스홀더 img 태그 제거 (깨진 이미지 방지)
1382 result = System.Text.RegularExpressions.Regex.Replace(
1383 result,
1384 @"<img\s[^>]*src=""[^""]*\‍[IMAGES[^\‍]]*\‍][^""]*""[^>]*/?>",
1385 "",
1386 System.Text.RegularExpressions.RegexOptions.IgnoreCase);
1387
1388 return result;
1389 }

다음에 의해서 참조됨 : ParseCookingResponseAsync(), ParseTravelResponse().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ TryExpandCollapsedPipeTable()

List< string > FamiliesAutoWriter.ViewModels.WriterSession.TryExpandCollapsedPipeTable ( string line)
inlinestaticprivate

Expand Collapsed Pipe Table 작업을 시도하고 성공 여부를 반환합니다.

매개변수
lineline에 사용할 string 값입니다.
반환값
Try Expand Collapsed Pipe Table 작업에서 생성한 List<string> 결과입니다.

WriterSession.cs 파일의 2215 번째 라인에서 정의되었습니다.

2216 {
2217 var trimmed = line.Trim();
2218 if (!trimmed.Contains('|') || !trimmed.Contains("---", StringComparison.Ordinal))
2219 return [];
2220
2221 var cells = trimmed
2222 .Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
2223 .Where(c => !string.IsNullOrWhiteSpace(c))
2224 .ToList();
2225
2226 var firstSeparator = cells.FindIndex(c => c.TrimStart().StartsWith("---", StringComparison.Ordinal));
2227 if (firstSeparator < 2) return [];
2228
2229 var columnCount = firstSeparator;
2230 if (columnCount < 2 || cells.Count < columnCount * 3)
2231 return [];
2232
2233 var separator = cells.Skip(columnCount).Take(columnCount).ToList();
2234 if (separator.Count != columnCount || separator.Any(c => !c.TrimStart().StartsWith("---", StringComparison.Ordinal)))
2235 return [];
2236
2237 var header = cells.Take(columnCount).Select(c => c.Trim()).ToArray();
2238 var values = cells.Skip(columnCount * 2).Select(c => c.Trim()).ToList();
2239 var dataRows = values
2240 .Chunk(columnCount)
2241 .Where(row => row.Length == columnCount)
2242 .Select(row => row.Select(c => c.Trim()).ToArray())
2243 .ToList();
2244
2245 if (dataRows.Count == 0)
2246 return [];
2247
2248 var result = new List<string>
2249 {
2250 "| " + string.Join(" | ", header) + " |",
2251 "| " + string.Join(" | ", Enumerable.Range(0, columnCount).Select(idx => idx == 1 ? "---:" : "---")) + " |"
2252 };
2253 result.AddRange(dataRows.Select(row => "| " + string.Join(" | ", row) + " |"));
2254 return result;
2255 }

다음에 의해서 참조됨 : NormalizeSimpleTables().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ UnwrapRaw()

string FamiliesAutoWriter.ViewModels.WriterSession.UnwrapRaw ( string? raw)
inlinestaticprivate

Unwrap Raw 작업을 수행합니다.

매개변수
rawraw에 사용할 string? 값입니다.
반환값
Unwrap Raw 작업에서 생성한 string 결과입니다.

WriterSession.cs 파일의 1263 번째 라인에서 정의되었습니다.

1264 {
1265 if (string.IsNullOrWhiteSpace(raw)) return "";
1266 try
1267 {
1268 var u = JsonSerializer.Deserialize<string>(raw) ?? raw;
1269 var d = JsonDocument.Parse(u);
1270 if (d.RootElement.TryGetProperty("text", out var tp)) return tp.GetString() ?? u;
1271 return u;
1272 }
1273 catch { return raw.Trim('"').Replace("\\n", "\n").Replace("\\\"", "\""); }
1274 }

다음에 의해서 참조됨 : ParseCookingResponseAsync(), ParseTravelResponse().

이 함수를 호출하는 함수들에 대한 그래프입니다.:

◆ ValidateImageUrlsAsync()

async Task< List< string > > FamiliesAutoWriter.ViewModels.WriterSession.ValidateImageUrlsAsync ( List< string > urls,
Action< string >? onStatus = null,
ISet< string >? excludeUrls = null )
inlinestaticprivate

Image Urls Async 값의 유효성을 검사합니다.

매개변수
urlsurls에 사용할 List<string> 값입니다.
onStatuson Status에 사용할 Action<string>? 값입니다.
excludeUrlsexclude Urls에 사용할 ISet<string>? 값입니다.
반환값
Validate Image Urls Async 작업에서 생성한 Task<List<string>> 결과입니다.

WriterSession.cs 파일의 2500 번째 라인에서 정의되었습니다.

2501 {
2502 var valid = new List<string>();
2503 foreach (var url in urls)
2504 {
2505 if (!IsPlausibleImageUrl(url))
2506 continue;
2507 if (excludeUrls != null && excludeUrls.Contains(url))
2508 continue;
2509
2510 try
2511 {
2512 // 신뢰 도메인 + 이미지 확장자 → HTTP 검증 생략
2513 if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
2514 {
2515 var host = uri.Host.ToLowerInvariant();
2516 var path = uri.AbsolutePath;
2517 bool trustedHost = _trustedImageHosts.Any(h => host.Contains(h, StringComparison.Ordinal));
2518 bool imageExt = Regex.IsMatch(path, @"\.(jpe?g|png|webp|gif|bmp)$", RegexOptions.IgnoreCase);
2519 if (trustedHost && imageExt)
2520 {
2521 valid.Add(url);
2522 continue;
2523 }
2524 }
2525
2526 onStatus?.Invoke($"🔍 {url[..Math.Min(60, url.Length)]}");
2527 if (await IsReachableImageAsync(url, HttpMethod.Head) ||
2528 await IsReachableImageAsync(url, HttpMethod.Get))
2529 {
2530 valid.Add(url);
2531 }
2532 }
2533 catch { }
2534 }
2535 return valid;
2536 }

다음을 참조함 : _trustedImageHosts, IsPlausibleImageUrl(), IsReachableImageAsync().

다음에 의해서 참조됨 : ExtractAndSaveAsync(), ParseCookingResponseAsync(), RunLoopCycleAsync().

이 함수 내부에서 호출하는 함수들에 대한 그래프입니다.:
이 함수를 호출하는 함수들에 대한 그래프입니다.:

멤버 데이터 문서화

◆ _aiWaitOption

WaitOption FamiliesAutoWriter.ViewModels.WriterSession._aiWaitOption = null!
private

ai Wait Option 값을 보관합니다.

WriterSession.cs 파일의 521 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : WriterSession().

◆ _albumIndex

int FamiliesAutoWriter.ViewModels.WriterSession._albumIndex
private

album Index 값을 보관합니다.

WriterSession.cs 파일의 227 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : RefreshAlbums(), RotateAlbum().

◆ _albumRotationEnabled

bool FamiliesAutoWriter.ViewModels.WriterSession._albumRotationEnabled
private

album Rotation Enabled 값을 보관합니다.

WriterSession.cs 파일의 316 번째 라인에서 정의되었습니다.

◆ _appDataRoot

string FamiliesAutoWriter.ViewModels.WriterSession._appDataRoot = DetectDefaultRoot()
private

app Data Root 값을 보관합니다.

WriterSession.cs 파일의 390 번째 라인에서 정의되었습니다.

◆ _cycleCount

int FamiliesAutoWriter.ViewModels.WriterSession._cycleCount
private

cycle Count 값을 보관합니다.

WriterSession.cs 파일의 209 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : RunLoopCycleAsync().

◆ _extractStatus

string FamiliesAutoWriter.ViewModels.WriterSession._extractStatus = ""
private

extract Status 값을 보관합니다.

WriterSession.cs 파일의 379 번째 라인에서 정의되었습니다.

◆ _history

readonly PromptHistoryService FamiliesAutoWriter.ViewModels.WriterSession._history = new()
private

history 값을 보관합니다.

WriterSession.cs 파일의 131 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : ClearPromptHistory().

◆ _http

readonly HttpClient FamiliesAutoWriter.ViewModels.WriterSession._http = new() { Timeout = TimeSpan.FromSeconds(15) }
staticprivate

http 값을 보관합니다.

WriterSession.cs 파일의 2356 번째 라인에서 정의되었습니다.

2356{ Timeout = TimeSpan.FromSeconds(15) };

다음에 의해서 참조됨 : IsReachableImageAsync().

◆ _loopCountdown

string FamiliesAutoWriter.ViewModels.WriterSession._loopCountdown = ""
private

loop Countdown 값을 보관합니다.

WriterSession.cs 파일의 343 번째 라인에서 정의되었습니다.

◆ _loopCts

CancellationTokenSource? FamiliesAutoWriter.ViewModels.WriterSession._loopCts
private

loop Cts 값을 보관합니다.

WriterSession.cs 파일의 191 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : StartLoop(), StopLoop().

◆ _loopEnabled

bool FamiliesAutoWriter.ViewModels.WriterSession._loopEnabled
private

loop Enabled 값을 보관합니다.

WriterSession.cs 파일의 459 번째 라인에서 정의되었습니다.

◆ _loopStatus

string FamiliesAutoWriter.ViewModels.WriterSession._loopStatus = "⏸ 루프 꺼짐"
private

loop Status 값을 보관합니다.

WriterSession.cs 파일의 334 번째 라인에서 정의되었습니다.

◆ _loopTask

Task? FamiliesAutoWriter.ViewModels.WriterSession._loopTask
private

loop Task 값을 보관합니다.

WriterSession.cs 파일의 200 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : StartLoop().

◆ _mediaPosition

MediaPosition FamiliesAutoWriter.ViewModels.WriterSession._mediaPosition = MediaPosition.Bottom
private

media Position 값을 보관합니다.

WriterSession.cs 파일의 352 번째 라인에서 정의되었습니다.

◆ _newChatEveryN

int FamiliesAutoWriter.ViewModels.WriterSession._newChatEveryN = 5
private

new Chat Every N 값을 보관합니다.

WriterSession.cs 파일의 325 번째 라인에서 정의되었습니다.

◆ _nextLoopAt

DateTime FamiliesAutoWriter.ViewModels.WriterSession._nextLoopAt = DateTime.MaxValue
private

next Loop At 값을 보관합니다.

WriterSession.cs 파일의 182 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : LoopWorkerAsync(), StartLoop(), StopLoop().

◆ _promptStatus

string FamiliesAutoWriter.ViewModels.WriterSession._promptStatus = ""
private

prompt Status 값을 보관합니다.

WriterSession.cs 파일의 370 번째 라인에서 정의되었습니다.

◆ _promptText

string FamiliesAutoWriter.ViewModels.WriterSession._promptText = BuildTravelPrompt([])
private

prompt Text 값을 보관합니다.

WriterSession.cs 파일의 361 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : WriterSession().

◆ _selectedAlbum

AlbumInfo? FamiliesAutoWriter.ViewModels.WriterSession._selectedAlbum
private

selected Album 값을 보관합니다.

WriterSession.cs 파일의 436 번째 라인에서 정의되었습니다.

◆ _selectedInterval

IntervalOption FamiliesAutoWriter.ViewModels.WriterSession._selectedInterval = null!
private

selected Interval 값을 보관합니다.

WriterSession.cs 파일의 498 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : WriterSession().

◆ _selectedSlug

string FamiliesAutoWriter.ViewModels.WriterSession._selectedSlug = ""
private

selected Slug 값을 보관합니다.

WriterSession.cs 파일의 413 번째 라인에서 정의되었습니다.

◆ _trustedImageHosts

readonly string [] FamiliesAutoWriter.ViewModels.WriterSession._trustedImageHosts
staticprivate
초기값:
=
[
"upload.wikimedia.org",
"images.unsplash.com",
"images.pexels.com",
"cdn.imweb.me",
"tong.visitkorea.or.kr",
"korean.visitkorea.or.kr",
"cdn.visitkorea.or.kr",
]

trusted Image Hosts 값을 보관합니다.

WriterSession.cs 파일의 2367 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : ValidateImageUrlsAsync().

◆ _uiDispatcher

readonly System.Windows.Threading.Dispatcher FamiliesAutoWriter.ViewModels.WriterSession._uiDispatcher
private

ui Dispatcher 값을 보관합니다.

WriterSession.cs 파일의 173 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : LoopWorkerAsync(), RunLoopCycleAsync(), SetStatus(), WriterSession().

◆ _usedImageUrls

readonly HashSet<string> FamiliesAutoWriter.ViewModels.WriterSession._usedImageUrls = new(StringComparer.OrdinalIgnoreCase)
private

used Image Urls 값을 보관합니다.

WriterSession.cs 파일의 218 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : ParseCookingResponseAsync().

◆ _writer

readonly PostWriterService FamiliesAutoWriter.ViewModels.WriterSession._writer = new()
private

writer 값을 보관합니다.

WriterSession.cs 파일의 122 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : ExtractAndSaveAsync(), ParseCookingResponseAsync(), RefreshAlbums(), RefreshPromptWithExisting(), RefreshSlugs(), RunLoopCycleAsync().

◆ ExtractionRetryDelaySeconds

const int FamiliesAutoWriter.ViewModels.WriterSession.ExtractionRetryDelaySeconds = 30
staticprivate

Extraction Retry Delay Seconds 값을 보관합니다.

WriterSession.cs 파일의 236 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : ExtractAndSaveAsync(), RunLoopCycleAsync().

◆ ExtractionRetryMaxAttempts

const int FamiliesAutoWriter.ViewModels.WriterSession.ExtractionRetryMaxAttempts = 10
staticprivate

Extraction Retry Max Attempts 값을 보관합니다.

WriterSession.cs 파일의 245 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : ExtractAndSaveAsync(), RunLoopCycleAsync().

속성 문서화

◆ AiWaitOption

WaitOption FamiliesAutoWriter.ViewModels.WriterSession.AiWaitOption
getset

Ai Wait Option 값을 가져오거나 설정합니다.

WriterSession.cs 파일의 530 번째 라인에서 정의되었습니다.

531 {
532 get => _aiWaitOption;
533 set => SetProperty(ref _aiWaitOption, value);
534 }

◆ Albums

ObservableCollection<AlbumInfo> FamiliesAutoWriter.ViewModels.WriterSession.Albums = []
get

Albums 값을 가져옵니다.

WriterSession.cs 파일의 265 번째 라인에서 정의되었습니다.

265{ get; } = [];

다음에 의해서 참조됨 : RefreshAlbums(), RotateAlbum().

◆ AppDataRoot

string FamiliesAutoWriter.ViewModels.WriterSession.AppDataRoot
getset

App Data Root 값을 가져오거나 설정합니다.

WriterSession.cs 파일의 399 번째 라인에서 정의되었습니다.

400 {
401 get => _appDataRoot;
402 set { if (SetProperty(ref _appDataRoot, value)) { _writer.AppDataRoot = value; RefreshSlugs(); } }
403 }

다음에 의해서 참조됨 : BrowseAppData(), RefreshSlugs().

◆ ExecuteScriptAsync

Func<string, Task<string?> >? FamiliesAutoWriter.ViewModels.WriterSession.ExecuteScriptAsync
getset

Execute Script Async 값을 가져오거나 설정합니다.

WriterSession.cs 파일의 153 번째 라인에서 정의되었습니다.

153{ get; set; }

다음에 의해서 참조됨 : ExtractAndSaveAsync(), RunLoopCycleAsync(), SendPromptNowAsync().

◆ IntervalMinutes

int FamiliesAutoWriter.ViewModels.WriterSession.IntervalMinutes
getprivate

Interval Minutes 값을 가져옵니다.

WriterSession.cs 파일의 684 번째 라인에서 정의되었습니다.

다음에 의해서 참조됨 : LoopWorkerAsync(), StartLoop().

◆ IntervalOptions

IntervalOption [] FamiliesAutoWriter.ViewModels.WriterSession.IntervalOptions
staticget
초기값:
=
[
new(5, "5분"), new(10, "10분"), new(15, "15분"),
new(20, "20분"), new(30, "30분"), new(60, "1시간"),
new(120, "2시간"), new(180,"3시간"), new(360,"6시간"),
]

Interval Options 값을 가져옵니다.

WriterSession.cs 파일의 276 번째 라인에서 정의되었습니다.

276{ get; } =

다음에 의해서 참조됨 : WriterSession().

◆ LoopEnabled

bool FamiliesAutoWriter.ViewModels.WriterSession.LoopEnabled
getset

Loop Enabled 값을 가져오거나 설정합니다.

WriterSession.cs 파일의 468 번째 라인에서 정의되었습니다.

469 {
470 get => _loopEnabled;
471 set
472 {
473 if (SetProperty(ref _loopEnabled, value))
474 {
475 OnPropertyChanged(nameof(LoopLabel));
476 if (value) StartLoop(); else StopLoop();
477 }
478 }
479 }

다음에 의해서 참조됨 : StopAutomation().

◆ LoopLabel

string FamiliesAutoWriter.ViewModels.WriterSession.LoopLabel
get

Loop Label 값을 가져옵니다.

WriterSession.cs 파일의 488 번째 라인에서 정의되었습니다.

◆ Mode

SessionMode FamiliesAutoWriter.ViewModels.WriterSession.Mode = SessionMode.Travel
getset

Mode 값을 가져오거나 설정합니다.

WriterSession.cs 파일의 142 번째 라인에서 정의되었습니다.

142{ get; set; } = SessionMode.Travel;

다음에 의해서 참조됨 : ExtractAndSaveAsync(), RefreshPromptWithExisting(), RunLoopCycleAsync().

◆ NavigateTab

Action<string>? FamiliesAutoWriter.ViewModels.WriterSession.NavigateTab
getset

Navigate Tab 값을 가져오거나 설정합니다.

WriterSession.cs 파일의 162 번째 라인에서 정의되었습니다.

162{ get; set; }

다음에 의해서 참조됨 : RunLoopCycleAsync().

◆ NewChatOptions

int [] FamiliesAutoWriter.ViewModels.WriterSession.NewChatOptions = [0, 3, 5, 10, 20, 30]
staticget

New Chat Options 값을 가져옵니다.

WriterSession.cs 파일의 305 번째 라인에서 정의되었습니다.

305{ get; } = [0, 3, 5, 10, 20, 30];

◆ SelectedAlbum

AlbumInfo? FamiliesAutoWriter.ViewModels.WriterSession.SelectedAlbum
getset

Selected Album 값을 가져오거나 설정합니다.

WriterSession.cs 파일의 445 번째 라인에서 정의되었습니다.

446 {
447 get => _selectedAlbum;
448 set => SetProperty(ref _selectedAlbum, value);
449 }

다음에 의해서 참조됨 : ExtractAndSaveAsync(), ParseCookingResponseAsync(), RefreshAlbums(), RotateAlbum(), RunLoopCycleAsync(), SendPromptNowAsync().

◆ SelectedInterval

IntervalOption FamiliesAutoWriter.ViewModels.WriterSession.SelectedInterval
getset

Selected Interval 값을 가져오거나 설정합니다.

WriterSession.cs 파일의 507 번째 라인에서 정의되었습니다.

508 {
509 get => _selectedInterval;
510 set { if (SetProperty(ref _selectedInterval, value) && LoopEnabled) StartLoop(); }
511 }

◆ SelectedSlug

string FamiliesAutoWriter.ViewModels.WriterSession.SelectedSlug
getset

Selected Slug 값을 가져오거나 설정합니다.

WriterSession.cs 파일의 422 번째 라인에서 정의되었습니다.

423 {
424 get => _selectedSlug;
425 set { if (SetProperty(ref _selectedSlug, value)) RefreshAlbums(); }
426 }

다음에 의해서 참조됨 : ExtractAndSaveAsync(), ParseCookingResponseAsync(), RefreshAlbums(), RefreshPromptWithExisting(), RefreshSlugs(), RunLoopCycleAsync().

◆ Slugs

ObservableCollection<string> FamiliesAutoWriter.ViewModels.WriterSession.Slugs = []
get

Slugs 값을 가져옵니다.

WriterSession.cs 파일의 256 번째 라인에서 정의되었습니다.

256{ get; } = [];

다음에 의해서 참조됨 : RefreshSlugs().

◆ WaitOptions

WaitOption [] FamiliesAutoWriter.ViewModels.WriterSession.WaitOptions
staticget
초기값:
=
[
new(15, "15초"), new(20, "20초"), new(30, "30초"),
new(45, "45초"), new(60, "1분"), new(90, "1분 30초"),
new(120, "2분"), new(150,"2분 30초"), new(180,"3분"),
new(210, "3분 30초"),
]

Wait Options 값을 가져옵니다.

WriterSession.cs 파일의 290 번째 라인에서 정의되었습니다.

290{ get; } =

다음에 의해서 참조됨 : WriterSession().


이 클래스에 대한 문서화 페이지는 다음의 파일로부터 생성되었습니다.: